@belticlabs/agent-risk-sdk 0.1.1 → 0.2.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.
@@ -1,7 +1,7 @@
1
- import { x402ResourceServer, x402HTTPResourceServer, PaywallConfig, RoutesConfig } from '@x402/core/server';
2
- import { B as Beltic } from './client-NxpD_384.js';
3
1
  import { D as Decision } from './index-Bjs3BPPU.js';
4
- import { c as SessionBorn, S as Session } from './session-B2mfurae.js';
2
+ import { x402ResourceServer, x402HTTPResourceServer } from '@x402/core/server';
3
+ import { B as Beltic } from './client-C-mV_3A0.js';
4
+ import { d as SessionBorn, S as Session } from './session-5TClPLI4.js';
5
5
 
6
6
  interface CorrelationContext {
7
7
  path: string;
@@ -25,16 +25,4 @@ interface AttachedX402 {
25
25
  }
26
26
  declare function attachX402(beltic: Beltic, server: x402ResourceServer, http?: x402HTTPResourceServer, opts?: AttachOptions): AttachedX402;
27
27
 
28
- /**
29
- * The seller half as one middleware: the framework's `@x402/*` payment
30
- * middleware over an `x402HTTPResourceServer` with the Beltic hooks
31
- * attached. `@belticlabs/agent-risk-sdk/hono` and `@belticlabs/agent-risk-sdk/express` differ only in
32
- * which `paymentMiddlewareFromHTTPServer` they hand in.
33
- */
34
-
35
- type GuardedMiddlewareOptions = AttachOptions & {
36
- paywall?: PaywallConfig | undefined;
37
- };
38
- declare function guardedPaymentMiddleware<M>(fromHTTPServer: (http: x402HTTPResourceServer, paywall?: PaywallConfig) => M, beltic: Beltic, routes: RoutesConfig, server: x402ResourceServer, opts?: GuardedMiddlewareOptions): M;
39
-
40
- export { type AttachOptions as A, type CorrelationContext as C, type GuardedMiddlewareOptions as G, attachX402 as a, type AttachedX402 as b, guardedPaymentMiddleware as g };
28
+ export { type AttachOptions as A, type CorrelationContext as C, attachX402 as a, type AttachedX402 as b };
@@ -1,6 +1,6 @@
1
1
  import { P as PaymentMomentPayload } from '../index-Bjs3BPPU.js';
2
2
  import { LanguageModelMiddleware, Tool, ToolSet } from 'ai';
3
- import { S as Session } from '../session-B2mfurae.js';
3
+ import { S as Session } from '../session-5TClPLI4.js';
4
4
  import 'zod';
5
5
 
6
6
  interface PaymentMoments {
@@ -12,8 +12,8 @@ interface WrapToolOptions<I = unknown, O = unknown> {
12
12
  /** When the tool performs a purchase, map its input/output to the payment moments it produced. */
13
13
  payment?: (input: I, output: O) => PaymentMoments | null;
14
14
  }
15
- declare function middleware(session: Session): LanguageModelMiddleware;
16
- declare function wrapTool<T extends Tool>(session: Session, tool: T, opts?: WrapToolOptions): T;
17
- declare function wrapTools<T extends ToolSet>(session: Session, tools: T, opts?: Record<string, WrapToolOptions>): T;
15
+ declare function middleware(session: Session | null | undefined): LanguageModelMiddleware;
16
+ declare function wrapTool<T extends Tool>(session: Session | null | undefined, tool: T, opts?: WrapToolOptions): T;
17
+ declare function wrapTools<T extends ToolSet>(session: Session | null | undefined, tools: T, opts?: Record<string, WrapToolOptions>): T;
18
18
 
19
19
  export { type PaymentMoments, type WrapToolOptions, middleware, wrapTool, wrapTools };
package/dist/ai/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  recordCall
3
- } from "../chunk-5TWO73OD.js";
3
+ } from "../chunk-GSH5APZW.js";
4
4
  import {
5
5
  uuidv7
6
6
  } from "../chunk-SFGM7KOG.js";
7
+ import "../chunk-46QN2KEZ.js";
7
8
  import {
8
9
  toJson,
9
10
  toJsonObject
10
11
  } from "../chunk-FQDHFTVR.js";
11
- import "../chunk-46QN2KEZ.js";
12
12
 
13
13
  // src/ai/index.ts
14
14
  var PARAM_KEYS = [
@@ -24,6 +24,7 @@ var PARAM_KEYS = [
24
24
  "stopSequences"
25
25
  ];
26
26
  function middleware(session) {
27
+ if (!session) return {};
27
28
  return {
28
29
  wrapGenerate: ({ doGenerate, params, model }) => recordCall(
29
30
  session,
@@ -73,7 +74,7 @@ function middleware(session) {
73
74
  };
74
75
  }
75
76
  function wrapTool(session, tool, opts = {}) {
76
- if (!tool.execute) return tool;
77
+ if (!session || !tool.execute) return tool;
77
78
  const original = tool.execute;
78
79
  const toolName = opts.name ?? tool.name ?? "tool";
79
80
  const execute = (input, options) => recordCall(
@@ -0,0 +1,39 @@
1
+ // src/core/record.ts
2
+ function errorOf(err) {
3
+ const e = err;
4
+ return { name: String(e?.name ?? "Error"), message: String(e?.message ?? err) };
5
+ }
6
+ function openCall(session, kind, callId, start) {
7
+ const started = Date.now();
8
+ const opened = session.emit(`${kind}.start`, { callId, ...start });
9
+ const close = async (outcome) => {
10
+ await opened;
11
+ return session.emit(
12
+ `${kind}.end`,
13
+ { callId, ...outcome, durationMs: Date.now() - started }
14
+ );
15
+ };
16
+ return {
17
+ callId,
18
+ opened,
19
+ end: (outcome = {}) => close(outcome),
20
+ fail: (error, outcome = {}) => close({ error: errorOf(error), ...outcome })
21
+ };
22
+ }
23
+ async function recordCall(session, kind, callId, start, run, end = () => ({})) {
24
+ const span = openCall(session, kind, callId, start);
25
+ await span.opened;
26
+ try {
27
+ const result = await run();
28
+ await span.end(await end(result));
29
+ return result;
30
+ } catch (err) {
31
+ await span.fail(err);
32
+ throw err;
33
+ }
34
+ }
35
+
36
+ export {
37
+ openCall,
38
+ recordCall
39
+ };
@@ -6,20 +6,23 @@ import {
6
6
  function x402Currency(network, asset) {
7
7
  return `${network}/${asset}`;
8
8
  }
9
- function fromAccepts(a, artifact, raw) {
9
+ function x402Summary(accepts, opts = {}) {
10
+ const payer = opts.payer?.toLowerCase();
10
11
  return {
11
12
  protocol: "x402",
12
- payee: a?.payTo ?? "unknown",
13
+ payee: accepts?.payTo ?? "unknown",
13
14
  amount: {
14
- value: a?.amount ?? "0",
15
- currency: a ? x402Currency(a.network ?? "unknown", a.asset ?? "unknown") : "unknown"
15
+ value: accepts?.amount ?? accepts?.maxAmountRequired ?? "0",
16
+ currency: accepts ? x402Currency(accepts.network ?? "unknown", accepts.asset ?? "unknown") : "unknown"
16
17
  },
17
- artifact,
18
- raw
18
+ ...payer ? { payer } : {}
19
19
  };
20
20
  }
21
+ function fromAccepts(a, artifact, raw, payer) {
22
+ return { ...x402Summary(a, { payer }), artifact, raw };
23
+ }
21
24
  function payerOf(payload) {
22
- const p = payload.payload;
25
+ const p = payload.payload ?? {};
23
26
  const auth = p.authorization;
24
27
  const candidates = [
25
28
  auth?.from,
@@ -32,15 +35,13 @@ function payerOf(payload) {
32
35
  return typeof hit === "string" ? hit.toLowerCase() : void 0;
33
36
  }
34
37
  var x402Moments = {
35
- /** The 402 challenge as the buyer saw it. */
38
+ /** The 402 challenge as the buyer saw it, v2 header or v1 body. */
36
39
  required(required) {
37
- const r = required;
38
- return fromAccepts(r.accepts[0], "http-402", toJsonObject(r));
40
+ return fromAccepts(required.accepts?.[0], "http-402", toJsonObject(required));
39
41
  },
40
42
  /** The requirements the seller's resource server resolved for a request. */
41
43
  requirements(req) {
42
- const r = req;
43
- return fromAccepts(r, "http-402", toJsonObject(r));
44
+ return fromAccepts(req, "http-402", toJsonObject(req));
44
45
  },
45
46
  /** A route's static `accepts` config, before any payment header exists. */
46
47
  route(route, raw) {
@@ -61,14 +62,18 @@ var x402Moments = {
61
62
  ask(first, raw) {
62
63
  return fromAccepts(first, "mrtr-input-required", raw);
63
64
  },
64
- /** The signed payment the buyer presented. */
65
- payload(payload) {
66
- const p = payload;
67
- const payer = payerOf(p);
68
- return {
69
- ...fromAccepts(p.accepted, "payment-signature", toJsonObject(p)),
70
- ...payer ? { payer } : {}
71
- };
65
+ /**
66
+ * The signed payment the buyer presented. A v2 payload carries the
67
+ * requirement it accepted; a v1 payload does not, so the caller passes
68
+ * the `accepts` entry it answered.
69
+ */
70
+ payload(payload, accepts) {
71
+ return fromAccepts(
72
+ accepts ?? payload.accepted,
73
+ "payment-signature",
74
+ toJsonObject(payload),
75
+ payerOf(payload)
76
+ );
72
77
  }
73
78
  };
74
79
  function priceValue(price) {
@@ -81,6 +86,7 @@ function priceValue(price) {
81
86
 
82
87
  export {
83
88
  x402Currency,
89
+ x402Summary,
84
90
  payerOf,
85
91
  x402Moments
86
92
  };
@@ -7,13 +7,7 @@ import {
7
7
  } from "./chunk-SFGM7KOG.js";
8
8
  import {
9
9
  x402Moments
10
- } from "./chunk-U5Z5Z2BQ.js";
11
- import {
12
- summaryOf
13
- } from "./chunk-7G5EHNVW.js";
14
- import {
15
- Verdict
16
- } from "./chunk-46QN2KEZ.js";
10
+ } from "./chunk-LM4NIYE5.js";
17
11
 
18
12
  // src/x402/adapter.ts
19
13
  function attachX402(beltic, server, http, opts = {}) {
@@ -56,16 +50,20 @@ function attachX402(beltic, server, http, opts = {}) {
56
50
  const presented = x402Moments.payload(payload);
57
51
  await session.emit("payment.presented", presented);
58
52
  inFlight.set(payloadDigest(payload), session);
59
- const out = await beltic.evaluate(session.id, summaryOf(presented));
53
+ const out = await beltic.evaluate(session.id, presented);
54
+ if (!out) return;
60
55
  opts.onDecision?.({
61
56
  sessionId: session.id,
62
57
  decision: out.decision,
63
58
  reasonCodes: out.reasonCodes,
64
59
  born: session.born
65
60
  });
66
- const verdict = Verdict.of(out.decision);
67
- if (verdict.blocks(beltic.onReview)) {
68
- return { abort: true, reason: `BELTIC_${verdict.value}`, message: out.reasonCodes.join(",") };
61
+ if (out.verdict.blocks(beltic.onReview)) {
62
+ return {
63
+ abort: true,
64
+ reason: `BELTIC_${out.verdict.value}`,
65
+ message: out.reasonCodes.join(",")
66
+ };
69
67
  }
70
68
  return;
71
69
  });
@@ -79,17 +77,6 @@ function attachX402(beltic, server, http, opts = {}) {
79
77
  return { inFlight };
80
78
  }
81
79
 
82
- // src/x402/middleware.ts
83
- import {
84
- x402HTTPResourceServer
85
- } from "@x402/core/server";
86
- function guardedPaymentMiddleware(fromHTTPServer, beltic, routes, server, opts = {}) {
87
- const http = new x402HTTPResourceServer(server, routes);
88
- attachX402(beltic, server, http, opts);
89
- return fromHTTPServer(http, opts.paywall);
90
- }
91
-
92
80
  export {
93
- attachX402,
94
- guardedPaymentMiddleware
81
+ attachX402
95
82
  };
@@ -0,0 +1,17 @@
1
+ import {
2
+ attachX402
3
+ } from "./chunk-SO6HPRJT.js";
4
+
5
+ // src/x402/middleware.ts
6
+ import {
7
+ x402HTTPResourceServer
8
+ } from "@x402/core/server";
9
+ function guardedPaymentMiddleware(fromHTTPServer, beltic, routes, server, opts = {}) {
10
+ const http = new x402HTTPResourceServer(server, routes);
11
+ attachX402(beltic, server, http, opts);
12
+ return fromHTTPServer(http, opts.paywall);
13
+ }
14
+
15
+ export {
16
+ guardedPaymentMiddleware
17
+ };
@@ -0,0 +1,91 @@
1
+ import { a as PaymentSummary, P as PaymentMomentPayload, B as EvaluateOutput } from './index-Bjs3BPPU.js';
2
+ import { O as OnReview, V as Verdict } from './verdict-BAahb5po.js';
3
+ import { a as ApiClient, g as Transport, e as Sessions, A as AgentIdentity, b as ApiClientOptions, i as TransportOptions, R as RedactFn } from './session-5TClPLI4.js';
4
+
5
+ /**
6
+ * Correlation without binding (Fraud SDK RFC › Protocol Adapter — x402:
7
+ * "binding travels on the call that initiates the purchase, not
8
+ * necessarily on the payment artifact"). The merchant binds a key it will
9
+ * see again (a checkout session id, a challenge nonce) to the buyer's
10
+ * session; the adapter resolves it when the settlement arrives (GAP-31).
11
+ */
12
+ interface CorrelationStore {
13
+ bind(key: string, sessionId: string, ttlMs?: number): Promise<void>;
14
+ resolve(key: string): Promise<string | null>;
15
+ }
16
+ declare class MemoryCorrelationStore implements CorrelationStore {
17
+ private readonly defaultTtlMs;
18
+ private readonly entries;
19
+ constructor(defaultTtlMs?: number);
20
+ bind(key: string, sessionId: string, ttlMs?: number): Promise<void>;
21
+ resolve(key: string): Promise<string | null>;
22
+ }
23
+
24
+ /**
25
+ * One SDK, two halves (Fraud SDK RFC › Summary). `Beltic` is the single
26
+ * client: sessions and evidence for both halves, `evaluate` for whichever
27
+ * half is about to let a payment through. Protocol integrations are plain
28
+ * functions behind subpath exports, each pulling exactly one optional peer:
29
+ *
30
+ * @belticlabs/agent-risk-sdk/ai → middleware(session), wrapTools(session, …)
31
+ * @belticlabs/agent-risk-sdk/x402 → belticFetch(session), x402Summary(…), attachX402(beltic, …)
32
+ * @belticlabs/agent-risk-sdk/hono → belticPaymentMiddleware(beltic, …) (and /express)
33
+ * @belticlabs/agent-risk-sdk/mcp → wrapClient(session, …), wrapServer(beltic, …)
34
+ *
35
+ * Neither half decides risk locally: verdicts are platform-side.
36
+ *
37
+ * Evidence is a side channel of the work it observes. With `failOpen`
38
+ * nothing the SDK does throws into that work: `Session.emit` reports and
39
+ * answers `false`, `sessions.open` answers `null`, `evaluate` answers
40
+ * `null` — never an invented verdict; the host decides what to do without
41
+ * one (GAP-70). `sessions.start` and `sessions.ensure` throw either way:
42
+ * they are the primitives the fail-open entries are built on.
43
+ */
44
+
45
+ declare const SDK_VERSION = "0.2.0";
46
+ interface BelticOptions extends Omit<ApiClientOptions, 'userAgent'> {
47
+ /** Buyer half. Without it, `sessions.start` is unavailable; the seller half works. */
48
+ identity?: AgentIdentity | undefined;
49
+ transport?: Partial<TransportOptions> | undefined;
50
+ /** What a synchronous seller hook does with REVIEW (GAP-52). */
51
+ onReview?: OnReview | undefined;
52
+ correlation?: CorrelationStore | undefined;
53
+ redact?: RedactFn | undefined;
54
+ now?: (() => Date) | undefined;
55
+ /** Evidence never fails the work it observes; see the module note (GAP-70). */
56
+ failOpen?: boolean | undefined;
57
+ /** Where fail-open failures go (and transport delivery failures unless `transport` names its own). Default: `console.error`. */
58
+ onError?: ((err: Error) => void) | undefined;
59
+ /** Fail-open only: how long `sessions.open` answers null after the platform refused to open a session (GAP-71). */
60
+ openRetryMs?: number | undefined;
61
+ }
62
+ /** The platform's answer, with the verdict as a value the caller can ask `blocks(onReview)`. */
63
+ type Evaluation = EvaluateOutput & {
64
+ verdict: Verdict;
65
+ };
66
+ declare class Beltic {
67
+ readonly api: ApiClient;
68
+ readonly transport: Transport;
69
+ readonly sessions: Sessions;
70
+ readonly identity: AgentIdentity | undefined;
71
+ readonly correlation: CorrelationStore;
72
+ readonly onReview: OnReview;
73
+ readonly failOpen: boolean;
74
+ private readonly onError;
75
+ constructor(opts: BelticOptions);
76
+ /**
77
+ * The platform's verdict on a payment — the seller's before it verifies,
78
+ * the buyer's before it presents. Read-your-writes: the buffered evidence
79
+ * is flushed first so the platform judges what the caller already saw
80
+ * (GAP-16). A recorded moment is accepted as is: only its comparable core
81
+ * (payee, amount, payer) is sent. `null` only under `failOpen`, when the
82
+ * platform could not be asked.
83
+ */
84
+ evaluate(sessionId: string, payment: PaymentSummary | PaymentMomentPayload): Promise<Evaluation | null>;
85
+ private decide;
86
+ flush(): Promise<void>;
87
+ shutdown(): Promise<void>;
88
+ }
89
+ declare function createBeltic(opts: BelticOptions): Beltic;
90
+
91
+ export { Beltic as B, type CorrelationStore as C, type Evaluation as E, MemoryCorrelationStore as M, SDK_VERSION as S, type BelticOptions as a, createBeltic as c };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { B as Beltic, a as BelticOptions, C as CorrelationStore, M as MemoryCorrelationStore, S as SDK_VERSION, c as createBeltic } from './client-NxpD_384.js';
2
- import { S as Session } from './session-B2mfurae.js';
3
- export { A as AgentIdentity, a as ApiClient, b as ApiClientOptions, B as BelticApiError, C as ChainRejectedError, D as DEFAULT_TRANSPORT, R as RedactFn, c as SessionBorn, d as Sessions, e as StartSessionInput, T as Transport, f as TransportClosedError, g as TransportOptions, h as ephemeralIdentity, i as fileIdentity, j as identityFromSeed } from './session-B2mfurae.js';
1
+ export { B as Beltic, a as BelticOptions, C as CorrelationStore, E as Evaluation, M as MemoryCorrelationStore, S as SDK_VERSION, c as createBeltic } from './client-C-mV_3A0.js';
2
+ import { S as Session } from './session-5TClPLI4.js';
3
+ export { A as AgentIdentity, a as ApiClient, b as ApiClientOptions, B as BelticApiError, C as ChainRejectedError, D as DEFAULT_OPEN_RETRY_MS, c as DEFAULT_TRANSPORT, O as OpenSessionInput, R as RedactFn, d as SessionBorn, e as Sessions, f as StartSessionInput, T as ToolCallSpan, g as Transport, h as TransportClosedError, i as TransportOptions, j as ephemeralIdentity, k as fileIdentity, l as identityFromSeed } from './session-5TClPLI4.js';
4
4
  import { a as PaymentSummary, J as JsonObject, P as PaymentMomentPayload } from './index-Bjs3BPPU.js';
5
5
  import './verdict-BAahb5po.js';
6
6
  import 'zod';
@@ -12,15 +12,16 @@ import 'zod';
12
12
  * sides of one purchase compare (`EVIDENCE_MISMATCH`, GAP-50).
13
13
  */
14
14
 
15
- declare function summaryOf(m: PaymentMomentPayload): PaymentSummary;
15
+ declare function summaryOf(m: PaymentSummary | PaymentMomentPayload): PaymentSummary;
16
16
  /** A presentation the seller side saw as a signed payment, in any protocol. */
17
17
  declare function presentedFrom(summary: PaymentSummary, raw: JsonObject): PaymentMomentPayload;
18
18
 
19
19
  /**
20
20
  * One instrumented call = a `*.start` event, the work, a `*.end` event
21
21
  * carrying the outcome or the error (GAP-07 correlates them by `callId`).
22
- * The AI middleware, the tool wrapper and the MCP client all record the
23
- * same way.
22
+ * `openCall` is the span; `recordCall` runs the work inside one. The AI
23
+ * middleware, the tool wrapper, the MCP client and a host's own tool loop
24
+ * (`Session.toolCall`) all record the same way.
24
25
  */
25
26
 
26
27
  type CallKind = 'llm_call' | 'tool_call';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
+ openCall,
2
3
  recordCall
3
- } from "./chunk-5TWO73OD.js";
4
+ } from "./chunk-GSH5APZW.js";
4
5
  import {
5
6
  Chain,
6
7
  didKeyFromEd25519,
@@ -8,11 +9,13 @@ import {
8
9
  memorySigner,
9
10
  toHex
10
11
  } from "./chunk-SFGM7KOG.js";
12
+ import {
13
+ Verdict
14
+ } from "./chunk-46QN2KEZ.js";
11
15
  import {
12
16
  presentedFrom,
13
17
  summaryOf
14
18
  } from "./chunk-7G5EHNVW.js";
15
- import "./chunk-46QN2KEZ.js";
16
19
 
17
20
  // src/core/api-client.ts
18
21
  var BelticApiError = class extends Error {
@@ -136,8 +139,26 @@ var Session = class {
136
139
  get droppedCount() {
137
140
  return this.dropped;
138
141
  }
139
- /** Resolves once the event is sequenced and buffered — not once it is acknowledged. */
142
+ /**
143
+ * Resolves once the event is sequenced and buffered — not once it is
144
+ * acknowledged. `false` when the event was dropped, or (fail-open) when
145
+ * the chain can no longer take it.
146
+ */
140
147
  async emit(kind, payload) {
148
+ if (!this.deps.failOpen) return this.chainEvent(kind, payload);
149
+ try {
150
+ return await this.chainEvent(kind, payload);
151
+ } catch (err) {
152
+ this.deps.onError?.(err);
153
+ return false;
154
+ }
155
+ }
156
+ /** The tool call whose `execute` the host runs itself; see `ToolCallSpan`. */
157
+ toolCall(call) {
158
+ const { callId, ...start } = call;
159
+ return openCall(this, "tool_call", callId, { transport: "local", ...start });
160
+ }
161
+ async chainEvent(kind, payload) {
141
162
  const halted = this.deps.transport.haltedError(this.id, this.source);
142
163
  if (halted) throw halted;
143
164
  const ts = this.now().toISOString();
@@ -186,6 +207,7 @@ var Session = class {
186
207
  return run;
187
208
  }
188
209
  };
210
+ var DEFAULT_OPEN_RETRY_MS = 6e4;
189
211
  var Sessions = class {
190
212
  constructor(deps) {
191
213
  this.deps = deps;
@@ -197,18 +219,65 @@ var Sessions = class {
197
219
  * mid-session still loses the head (GAP-67).
198
220
  */
199
221
  attached = /* @__PURE__ */ new Map();
222
+ /** Buyer sessions by the host's own key (GAP-71). */
223
+ opened = /* @__PURE__ */ new Map();
224
+ retryAt = 0;
225
+ /**
226
+ * Buyer half: the evidence session for a key of the host's own (its
227
+ * session, run or conversation id), opened on first use and reused
228
+ * after. A halted chain is reopened as a fresh session that continues
229
+ * the same key; a closed key is forgotten. When the platform refuses to
230
+ * open one, a fail-open client resolves null — the host runs without
231
+ * evidence — until `openRetryMs` has passed (GAP-71); otherwise the
232
+ * refusal is thrown and the next call tries again. The identity is
233
+ * required either way: that is configuration.
234
+ */
235
+ open(key, input = {}) {
236
+ const prior = this.opened.get(key) ?? Promise.resolve(null);
237
+ const next = prior.catch(() => null).then(
238
+ (session) => session && !this.deps.transport.haltedError(session.id, session.source) ? session : this.openFresh(input, () => this.forget(key, next))
239
+ );
240
+ this.opened.set(key, next);
241
+ next.catch(() => this.forget(key, next));
242
+ return next;
243
+ }
244
+ forget(key, entry) {
245
+ if (this.opened.get(key) === entry) this.opened.delete(key);
246
+ }
247
+ async openFresh(input, onClosed) {
248
+ this.identityFor("open");
249
+ const create = async () => this.create(typeof input === "function" ? await input() : input, onClosed);
250
+ if (!this.deps.failOpen) return create();
251
+ if (Date.now() < this.retryAt) return null;
252
+ try {
253
+ const session = await create();
254
+ this.retryAt = 0;
255
+ return session;
256
+ } catch (err) {
257
+ this.retryAt = Date.now() + (this.deps.openRetryMs ?? DEFAULT_OPEN_RETRY_MS);
258
+ this.deps.onError?.(err);
259
+ return null;
260
+ }
261
+ }
200
262
  /** Buyer half: create an AGENT_TRACE session bound to the agent identity, then announce it on the chain. */
201
- async start(input = {}) {
263
+ start(input = {}) {
264
+ return this.create(input);
265
+ }
266
+ identityFor(entry) {
202
267
  const identity = this.deps.identity;
203
268
  if (!identity)
204
- throw new Error("sessions.start needs an agent identity (createBeltic({ identity }))");
269
+ throw new Error(`sessions.${entry} needs an agent identity (createBeltic({ identity }))`);
270
+ return identity;
271
+ }
272
+ async create(input, onClosed) {
273
+ const identity = this.identityFor("start");
205
274
  const body = {
206
275
  source: "AGENT_TRACE",
207
276
  agent: { did: identity.did, credential: identity.credential ?? identity.did },
208
277
  ...input.intent ? { intent: input.intent } : {}
209
278
  };
210
279
  const out = await this.deps.api.post("/v1/sessions", body);
211
- const session = this.attach(out.sessionId, "AGENT_TRACE", out.expiresAt, "buyer");
280
+ const session = this.attach(out.sessionId, "AGENT_TRACE", out.expiresAt, "buyer", onClosed);
212
281
  await session.emit("session.open", {
213
282
  runtime: {
214
283
  sdk: "@belticlabs/agent-risk-sdk",
@@ -228,7 +297,7 @@ var Sessions = class {
228
297
  });
229
298
  return this.attach(out.sessionId, "INTERNAL_NETWORK", out.expiresAt, "seller");
230
299
  }
231
- attach(id, source, expiresAt, born) {
300
+ attach(id, source, expiresAt, born, onClosed) {
232
301
  const key = `${id}:${source}`;
233
302
  const existing = this.attached.get(key);
234
303
  if (existing) return existing;
@@ -238,7 +307,12 @@ var Sessions = class {
238
307
  signer: source === "AGENT_TRACE" ? this.deps.identity?.signer : void 0,
239
308
  redact: this.deps.redact,
240
309
  now: this.deps.now,
241
- onClosed: () => this.attached.delete(key)
310
+ failOpen: this.deps.failOpen,
311
+ onError: this.deps.onError,
312
+ onClosed: () => {
313
+ this.attached.delete(key);
314
+ onClosed?.();
315
+ }
242
316
  },
243
317
  id,
244
318
  source,
@@ -412,7 +486,7 @@ var Transport = class {
412
486
  };
413
487
 
414
488
  // src/client.ts
415
- var SDK_VERSION = "0.1.1";
489
+ var SDK_VERSION = "0.2.0";
416
490
  var Beltic = class {
417
491
  api;
418
492
  transport;
@@ -420,28 +494,54 @@ var Beltic = class {
420
494
  identity;
421
495
  correlation;
422
496
  onReview;
497
+ failOpen;
498
+ onError;
423
499
  constructor(opts) {
500
+ this.failOpen = opts.failOpen ?? false;
501
+ this.onError = opts.onError ?? ((err) => console.error("[beltic]", err));
424
502
  this.api = new ApiClient({ ...opts, userAgent: `@belticlabs/agent-risk-sdk/${SDK_VERSION}` });
425
- this.transport = new Transport(this.api, opts.transport);
503
+ this.transport = new Transport(this.api, {
504
+ onError: this.onError,
505
+ onChainHalted: this.onError,
506
+ ...opts.transport
507
+ });
426
508
  this.sessions = new Sessions({
427
509
  api: this.api,
428
510
  transport: this.transport,
429
511
  sdkVersion: SDK_VERSION,
430
512
  identity: opts.identity,
431
513
  redact: opts.redact,
432
- now: opts.now
514
+ now: opts.now,
515
+ failOpen: this.failOpen,
516
+ onError: this.onError,
517
+ openRetryMs: opts.openRetryMs
433
518
  });
434
519
  this.identity = opts.identity;
435
520
  this.correlation = opts.correlation ?? new MemoryCorrelationStore();
436
521
  this.onReview = opts.onReview ?? "abort";
437
522
  }
438
- /** Read-your-writes: the platform must hold the evidence before it judges it (GAP-16). */
523
+ /**
524
+ * The platform's verdict on a payment — the seller's before it verifies,
525
+ * the buyer's before it presents. Read-your-writes: the buffered evidence
526
+ * is flushed first so the platform judges what the caller already saw
527
+ * (GAP-16). A recorded moment is accepted as is: only its comparable core
528
+ * (payee, amount, payer) is sent. `null` only under `failOpen`, when the
529
+ * platform could not be asked.
530
+ */
439
531
  async evaluate(sessionId, payment) {
532
+ const input = { sessionId, payment: summaryOf(payment) };
533
+ if (!this.failOpen) return this.decide(input);
534
+ try {
535
+ return await this.decide(input);
536
+ } catch (err) {
537
+ this.onError(err);
538
+ return null;
539
+ }
540
+ }
541
+ async decide(input) {
440
542
  await this.transport.flush();
441
- return this.api.post("/v1/evaluate", {
442
- sessionId,
443
- payment
444
- });
543
+ const out = await this.api.post("/v1/evaluate", input);
544
+ return { ...out, verdict: Verdict.of(out.decision) };
445
545
  }
446
546
  flush() {
447
547
  return this.transport.flush();
@@ -482,6 +582,7 @@ export {
482
582
  Beltic,
483
583
  BelticApiError,
484
584
  ChainRejectedError,
585
+ DEFAULT_OPEN_RETRY_MS,
485
586
  DEFAULT_TRANSPORT,
486
587
  MemoryCorrelationStore,
487
588
  SDK_VERSION,
@@ -1,6 +1,6 @@
1
1
  import { a as PaymentSummary, P as PaymentMomentPayload } from '../index-Bjs3BPPU.js';
2
- import { B as Beltic } from '../client-NxpD_384.js';
3
- import { S as Session } from '../session-B2mfurae.js';
2
+ import { B as Beltic } from '../client-C-mV_3A0.js';
3
+ import { S as Session } from '../session-5TClPLI4.js';
4
4
  import 'zod';
5
5
  import '../verdict-BAahb5po.js';
6
6
 
@@ -13,7 +13,7 @@ interface CallToolParams {
13
13
  interface McpClientLike {
14
14
  callTool(params: CallToolParams, ...rest: unknown[]): Promise<unknown>;
15
15
  }
16
- declare function wrapClient<C extends McpClientLike>(session: Session, client: C, opts?: {
16
+ declare function wrapClient<C extends McpClientLike>(session: Session | null | undefined, client: C, opts?: {
17
17
  server?: string;
18
18
  }): C;
19
19
  /**