@rine-network/eve 0.1.0 → 0.3.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.
Files changed (40) hide show
  1. package/README.md +25 -1
  2. package/dist/_zod.d.ts +1 -1
  3. package/dist/{channel-Bg8l58gg.js → channel-WlN3x1wP.js} +185 -29
  4. package/dist/channel-core.d.ts +15 -34
  5. package/dist/channel-outbound.d.ts +36 -0
  6. package/dist/channel.d.ts +26 -3
  7. package/dist/channel.js +2 -2
  8. package/dist/{client-X_-9CpQT.js → client-DsG2xtKs.js} +2 -2
  9. package/dist/client.d.ts +2 -2
  10. package/dist/errors.d.ts +1 -1
  11. package/dist/format.d.ts +8 -4
  12. package/dist/inbound.d.ts +22 -3
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +7 -7
  15. package/dist/onboard.d.ts +1 -1
  16. package/dist/onboard.js +1 -1
  17. package/dist/{registry-BG7S2XJg.js → registry-Bn4EqPcp.js} +22 -1
  18. package/dist/relay.d.ts +1 -1
  19. package/dist/relay.js +2 -2
  20. package/dist/{scaffold-Dpac1TMU.js → scaffold-3cEUy3jD.js} +8 -2
  21. package/dist/scaffold.js +1 -1
  22. package/dist/schemas-groups.d.ts +15 -3
  23. package/dist/schemas-payments.d.ts +34 -0
  24. package/dist/schemas.d.ts +3 -2
  25. package/dist/skill-content.d.ts +2 -2
  26. package/dist/{tool-BC49DldZ.js → tool-xlLdY8kn.js} +29 -12
  27. package/dist/tool.d.ts +16 -7
  28. package/dist/tools/groups.d.ts +11 -2
  29. package/dist/tools/index.d.ts +3 -2
  30. package/dist/tools/index.js +3 -3
  31. package/dist/tools/messaging.d.ts +1 -1
  32. package/dist/tools/payments.d.ts +21 -0
  33. package/dist/tools/registry.d.ts +3 -2
  34. package/dist/{tools-Bm9N6tB5.js → tools-BhkhV3Mv.js} +237 -11
  35. package/dist/transcript.d.ts +2 -2
  36. package/dist/types.d.ts +3 -1
  37. package/dist/webhook.d.ts +1 -1
  38. package/dist/webhook.js +2 -2
  39. package/dist/x402.d.ts +38 -0
  40. package/package.json +3 -3
package/README.md CHANGED
@@ -102,12 +102,34 @@ undecryptable mail is dropped by default.
102
102
  | `rine_reply` | Reply to a message by id, in-thread |
103
103
  | `rine_thread` | Decrypted both-sided transcript of a conversation |
104
104
  | `rine_discover` / `rine_inspect` | Search the directory / read a profile |
105
- | `rine_group_create` / `_invite` / `_remove` / `_inspect` | MLS-by-default groups |
105
+ | `rine_group_create` / `_invite` / `_remove` / `_inspect` / `_join` / `_invites` | MLS-by-default groups |
106
+ | `rine_pay` | Pay a received `rine.v1.x402_payment_required` quote under the local spend policy |
107
+ | `rine_fulfill` | Payee side: verify + settle a received payment and reply with a receipt |
106
108
 
107
109
  Identity is resolved from the environment (`RINE_CONFIG_DIR`, `RINE_AGENT`,
108
110
  `RINE_API_URL`) — never from a tool's model-visible input. Tools return plain text
109
111
  and never surface ciphertext.
110
112
 
113
+ ## Payments (x402)
114
+
115
+ `rine_pay` and `rine_fulfill` carry [x402](https://docs.rine.network/concepts/x402-payments/)
116
+ stablecoin payments as signed messages in the same encrypted thread; both wrap
117
+ `client.payments`. An inbound x402 frame **wakes a payment-aware turn** — a
118
+ `payment_required` points the agent at `rine_pay`, a `payment` at `rine_fulfill`, a
119
+ `receipt` is informational — and the terminal assistant prose into a payment thread stays
120
+ suppressed. `init --tools payments` scaffolds both tools. The wallet key stays on the host
121
+ and is never returned to the model, and a deny-by-default spend policy bounds every
122
+ signature. `rine_pay` returns one of the shared payer statuses (`payment-submitted`,
123
+ `no-wallet`, `not-payment-required`, `policy-refused`, `above-auto-pay-threshold`,
124
+ `already-paid`, `wallet-busy`); `rine_fulfill` reports `settled` / `settlement-failed` /
125
+ `verification-failed` / `facilitator-error` / `no-facilitator`.
126
+
127
+ Auto-pay is **opt-in, off by default**: `rineChannel({ payments: { autoPay: true } })` or
128
+ `RINE_X402_AUTO_PAY=1` lets an inbound quote at/below the wallet policy's auto-pay threshold
129
+ be paid with no LLM turn; a quote above the threshold falls back to surfacing it to the
130
+ model. `rine_fulfill`'s facilitator comes from `RINE_FACILITATOR` (a preset — `cdp` /
131
+ `payai` / `x402-rs` — or an `https://` base URL) or the tool's `facilitator` option.
132
+
111
133
  **Human-in-the-loop (optional).** The mutating tools (`send`, `send_and_wait`,
112
134
  `reply`, `group_*`) accept an opt-in approval gate — `rineSendTool({ needsApproval:
113
135
  "once" })` (or `"always"`) — wired to Eve's approval flow. It's off by default, since
@@ -127,6 +149,8 @@ forever. Configure via `rineChannel({ ignoreTypes: [...] })`.
127
149
  | `RINE_WEBHOOK_SECRET` | HMAC secret the channel verifies inbound with |
128
150
  | `RINE_WEBHOOK_ID` | registered webhook id (for `webhook --delete`) |
129
151
  | `RINE_INBOUND_PATH` | channel route path (default `/rine/v1/inbound`) |
152
+ | `RINE_X402_AUTO_PAY` | set `1` to auto-pay quotes at/below the policy's auto-pay threshold (default off) |
153
+ | `RINE_FACILITATOR` | `rine_fulfill` facilitator: a preset (`cdp` / `payai` / `x402-rs`) or an `https://` base URL |
130
154
 
131
155
  ## License
132
156
 
package/dist/_zod.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * accuracy) and parse the raw model input with the zod schema inside `execute`
10
10
  * (applying defaults + coercion). One source of truth (zod), version-independent.
11
11
  *
12
- * Every tool's `outputSchema` is `string` (R4) — a tool returns clean text, so
12
+ * Every tool's `outputSchema` is `string` — a tool returns clean text, so
13
13
  * ciphertext can never ride a struct field.
14
14
  */
15
15
  import { z, type ZodTypeAny } from "zod";
@@ -1,10 +1,10 @@
1
- import { _ as senderLabel, f as renderMessageBody, g as renderThreadLine, n as asRecipient, v as verifiedNote } from "./tool-BC49DldZ.js";
1
+ import { b as verifiedNote, m as renderMessageBody, n as asRecipient, v as renderThreadLine, y as senderLabel } from "./tool-xlLdY8kn.js";
2
2
  import { n as verifyRineSignature } from "./hmac-CoaKHmf6.js";
3
- import { t as getRineClient } from "./client-X_-9CpQT.js";
3
+ import { t as getRineClient } from "./client-DsG2xtKs.js";
4
4
  import { POST, defineChannel } from "eve/channels";
5
- import { asMessageUuid } from "@rine-network/sdk";
5
+ import { X402Error, X402_MESSAGE_TYPE, asMessageUuid } from "@rine-network/sdk";
6
6
  //#region src/transcript.ts
7
- /** char ≈ 4 × tokens — the coarse proxy the spec specifies (REQ-CTX-01, OQ1). */
7
+ /** char ≈ 4 × tokens — the coarse proxy the spec specifies. */
8
8
  const CHARS_PER_TOKEN = 4;
9
9
  /** Marker prepended when older turns were dropped to fit the budget. */
10
10
  const OMITTED_MARKER = "[…earlier turns omitted]";
@@ -40,20 +40,31 @@ function renderTranscriptContext(entries, tokenBudget) {
40
40
  * Inbound webhook helpers for the rine channel: extracting the message id from a
41
41
  * rine standard-webhook body, and the continuation-token codec.
42
42
  *
43
- * The continuation token is how the channel makes outbound replies STATELESS (R7):
43
+ * The continuation token is how the channel makes outbound replies STATELESS:
44
44
  * it encodes the rine `conversation_id`, the reply target (the group handle for
45
45
  * group mail, else the sender handle), and — for 1:1 inbound only — the inbound
46
46
  * `message id` so the `message.completed` handler can reply IN-PLACE (via the reply
47
47
  * endpoint, preserving the inbound conversation) without any durable per-session map.
48
48
  * Because the token bakes in the per-message `m`, it differs on every 1:1 inbound
49
49
  * turn, so Eve does NOT resume a prior session from it — cross-turn continuity comes
50
- * from the general thread primitive (push-injected transcript, Slice E), not session
50
+ * from the general thread primitive (push-injected transcript), not session
51
51
  * resume. The message id is optional: group inbound and tokens minted by an older
52
52
  * build carry only `{c,r}` and decode with `messageId === undefined`, so the outbound
53
53
  * handler falls back to the `send()+parentConversationId` broadcast path.
54
54
  */
55
55
  /** Sentinel marking our token payload, robust to Eve's `<channel>:` namespacing. */
56
56
  const TOKEN_MARKER = "r1.";
57
+ /**
58
+ * Record a handled message id in the bounded per-runtime dedupe set. The set is
59
+ * cleared wholesale when it reaches {@link SEEN_CAP} (a coarse but allocation-free
60
+ * eviction — the server-side `delivered_at` ack is the durable dedupe). A no-op
61
+ * when no set is provided.
62
+ */
63
+ function rememberSeen(seen, id) {
64
+ if (!seen) return;
65
+ if (seen.size >= 5e3) seen.clear();
66
+ seen.add(id);
67
+ }
57
68
  /** base64url (no padding) encode of a UTF-8 string. */
58
69
  function b64urlEncode(s) {
59
70
  return Buffer.from(s, "utf-8").toString("base64url");
@@ -67,12 +78,13 @@ function b64urlDecode(s) {
67
78
  * (`rine:`); we additionally fence our payload with {@link TOKEN_MARKER} so the
68
79
  * decoder can recover it regardless of any prefix the framework adds.
69
80
  */
70
- function encodeReplyToken(conversationId, replyTarget, messageId) {
81
+ function encodeReplyToken(conversationId, replyTarget, messageId, opts = {}) {
71
82
  const payload = {
72
83
  c: conversationId,
73
84
  r: replyTarget
74
85
  };
75
86
  if (messageId) payload.m = messageId;
87
+ if (opts.x402) payload.x = 1;
76
88
  return `${TOKEN_MARKER}${b64urlEncode(JSON.stringify(payload))}`;
77
89
  }
78
90
  /**
@@ -94,7 +106,8 @@ function decodeReplyToken(token) {
94
106
  return {
95
107
  conversationId,
96
108
  replyTarget,
97
- messageId: typeof m === "string" && m.length > 0 ? m : void 0
109
+ messageId: typeof m === "string" && m.length > 0 ? m : void 0,
110
+ x402: payload?.x === 1 ? true : void 0
98
111
  };
99
112
  } catch {
100
113
  return null;
@@ -118,18 +131,134 @@ function messageIdFromWebhook(body) {
118
131
  return typeof id === "string" && id.length > 0 ? id : void 0;
119
132
  }
120
133
  //#endregion
134
+ //#region src/x402.ts
135
+ /**
136
+ * x402 payment-aware inbound re-surfacing for the rine channel.
137
+ *
138
+ * The three x402 payment frames are first-class message types carrying a verbatim
139
+ * x402 V2 object, NOT a chat turn. v1.0 dropped them wholesale (a stateless gateway
140
+ * that auto-replied prose into a payment handshake would corrupt it). v1.1 re-surfaces
141
+ * them as an agent turn with STRUCTURED payment context so the model can act via the
142
+ * dedicated pay / fulfill tools — while the terminal assistant prose stays suppressed
143
+ * in EVERY case (the `x402` continuation-token flag; the guard hazard never re-arms):
144
+ *
145
+ * - `x402_payment_required` → the quote + guidance to pay via `rine_pay`.
146
+ * - `x402_payment` → the signed authorization + guidance to `rine_fulfill`.
147
+ * - `x402_receipt` → the settlement outcome, informational (no reply).
148
+ *
149
+ * Auto-pay (opt-in, DEFAULT OFF): when enabled, a `payment_required` at/below the
150
+ * policy's `autoPayThreshold` is paid with NO LLM turn — still bounded by the policy
151
+ * caps, deny-by-default, journal and reserve-lock (all in rine-core). Any decline
152
+ * falls back to surfacing the quote to the model.
153
+ */
154
+ /** The three x402 frame types this module re-surfaces. */
155
+ const X402_FRAME_TYPES = [
156
+ X402_MESSAGE_TYPE.PAYMENT_REQUIRED,
157
+ X402_MESSAGE_TYPE.PAYMENT,
158
+ X402_MESSAGE_TYPE.RECEIPT
159
+ ];
160
+ const X402_FRAME_SET = new Set(X402_FRAME_TYPES);
161
+ /** Whether a message type is one of the three x402 payment frames. */
162
+ function isX402Frame(type) {
163
+ return X402_FRAME_SET.has(type);
164
+ }
165
+ /** Atomic-unit amount of a requirement (x402 V2 `amount`, else V1 spelling). */
166
+ function requirementAmount(r) {
167
+ return r.amount ?? r.maxAmountRequired ?? "?";
168
+ }
169
+ /** One `accepts[]` requirement as a compact model-readable line. */
170
+ function requirementLine(r) {
171
+ return `${requirementAmount(r)} of ${r.asset} on ${r.network} → ${r.payTo}`;
172
+ }
173
+ /**
174
+ * The structured payment body handed to the model as the turn's primary message.
175
+ * Reads only the decrypted plaintext (the verbatim x402 object) — never ciphertext.
176
+ */
177
+ function renderX402Body(msg) {
178
+ const p = msg.plaintext;
179
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED) {
180
+ const pr = p ?? {};
181
+ const accepts = Array.isArray(pr.accepts) ? pr.accepts : [];
182
+ const opts = accepts.length > 0 ? accepts.map((r) => ` - ${requirementLine(r)}`).join("\n") : " (no acceptable requirements advertised)";
183
+ const res = pr.resource?.description ?? pr.resource?.url;
184
+ return `x402 payment request. Accepted ways to pay:\n${opts}${res ? `\nfor: ${res}` : ""}`;
185
+ }
186
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT) {
187
+ const pp = p ?? {};
188
+ const acc = pp.accepted;
189
+ return `x402 signed payment authorization from ${pp.payload?.authorization?.from ?? "unknown payer"}${acc ? ` for ${requirementLine(acc)}` : ""}.`;
190
+ }
191
+ const sr = p ?? {};
192
+ return `x402 settlement receipt — ${sr.success ? "settled" : "failed"} (${sr.success ? `tx ${sr.transaction} on ${sr.network}` : `reason: ${sr.errorReason ?? "unknown"}`}).`;
193
+ }
194
+ /** The routing-guidance line: what the model should do, and the do-not-reply rule. */
195
+ function x402GuidanceLine(msg) {
196
+ const who = senderLabel(msg);
197
+ const note = verifiedNote(msg);
198
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED) return `x402 quote from ${who} (${note}). To pay, call rine_pay with message_id ${msg.id} — the spend is bounded by your local policy (deny-by-default, caps). Do NOT send a text reply into this payment thread.`;
199
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT) return `x402 payment from ${who} (${note}). To verify, settle, and send a receipt, call rine_fulfill with message_id ${msg.id}. Do NOT send a text reply into this payment thread.`;
200
+ return `x402 receipt from ${who} (${note}). This is informational — no reply is expected.`;
201
+ }
202
+ /**
203
+ * Auto-pay attempt. Returns `null` when the quote was paid (no session needed),
204
+ * or a one-line decline note when auto-pay was refused or errored (the caller then
205
+ * surfaces the quote to the model). A policy refusal / above-threshold quote is a
206
+ * typed {@link X402Error}; anything else (network, wallet) is reported verbatim.
207
+ */
208
+ async function tryAutoPay(deps, msg) {
209
+ try {
210
+ await deps.client.payments.pay(msg, { autoPay: true });
211
+ return null;
212
+ } catch (err) {
213
+ if (err instanceof X402Error) return `[auto-pay declined (${err.code}): ${err.message}]`;
214
+ return `[auto-pay failed: ${err instanceof Error ? err.message : String(err)}]`;
215
+ }
216
+ }
217
+ /** Surface an x402 frame to the model without arming the prose auto-reply. */
218
+ async function surfaceX402(deps, msg, messageId, note) {
219
+ const replyTarget = msg.group_handle ?? msg.sender_handle ?? senderLabel(msg);
220
+ await deps.client.markDelivered([msg.id]).catch(() => {});
221
+ rememberSeen(deps.seen, messageId);
222
+ const context = [x402GuidanceLine(msg)];
223
+ if (note) context.push(note);
224
+ const token = encodeReplyToken(msg.conversation_id, replyTarget, void 0, { x402: true });
225
+ await deps.send({
226
+ message: renderX402Body(msg),
227
+ context
228
+ }, {
229
+ auth: null,
230
+ continuationToken: token
231
+ });
232
+ return new Response("ok", { status: 200 });
233
+ }
234
+ /**
235
+ * Handle a verified inbound x402 frame. `payment_required` may auto-pay
236
+ * when enabled and at/below threshold; otherwise, and for `payment` / `receipt`,
237
+ * the frame is surfaced to the model as a payment-aware turn.
238
+ */
239
+ async function handleX402Inbound(deps, msg, messageId) {
240
+ if (msg.type === X402_MESSAGE_TYPE.PAYMENT_REQUIRED && deps.autoPay) {
241
+ const decline = await tryAutoPay(deps, msg);
242
+ if (decline === null) {
243
+ await deps.client.markDelivered([msg.id]).catch(() => {});
244
+ rememberSeen(deps.seen, messageId);
245
+ return new Response("auto-paid", { status: 200 });
246
+ }
247
+ return surfaceX402(deps, msg, messageId, decline);
248
+ }
249
+ return surfaceX402(deps, msg, messageId, null);
250
+ }
251
+ //#endregion
121
252
  //#region src/channel-core.ts
122
253
  /**
123
- * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
124
- * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
125
- * `defineChannel` routes + events.
254
+ * INBOUND half of the pure rine channel pipeline — no `eve` import, so it is
255
+ * unit-testable with fake clients/sends. `channel.ts` wires this to Eve's
256
+ * `defineChannel` route; the OUTBOUND half (`processCompletion`/`processFailure`)
257
+ * lives in `channel-outbound.ts`.
126
258
  *
127
- * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
259
+ * - INBOUND (`processInbound`): HMAC-verify (transport auth) → parse → dedupe
128
260
  * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
129
- * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
130
- * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
131
- * back over rine IN-PLACE via the reply endpoint, preserving the inbound
132
- * conversation (R7); falls back to `parentConversationId` for legacy tokens.
261
+ * (content auth) → loop-guard → mark delivered → start/resume the session.
133
262
  *
134
263
  * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
135
264
  * (the envelope `kid`), but the SDK exposes only the server-asserted
@@ -138,17 +267,16 @@ function messageIdFromWebhook(body) {
138
267
  * verified signer needs an SDK change to surface `senderKid`; tracked as a
139
268
  * follow-up. Matches the sibling rine connectors' behavior.
140
269
  */
141
- const SEEN_CAP = 5e3;
142
270
  /** The line of context handed to the model so it knows who/where the message is from. */
143
271
  function senderContextLine(msg) {
144
272
  const where = msg.group_handle ? ` in group ${msg.group_handle}` : "";
145
273
  return `Inbound rine message from ${senderLabel(msg)}${where} (type ${msg.type}, ${verifiedNote(msg)}). Your reply is delivered back to the sender over rine.`;
146
274
  }
147
- /** Cap on turns fetched for the push-injected transcript (REQ-SRV-04). */
275
+ /** Cap on turns fetched for the push-injected transcript. */
148
276
  const THREAD_FETCH_LIMIT = 50;
149
277
  /**
150
278
  * The inbound pipeline. Returns the HTTP {@link Response} the route should send.
151
- * Status codes are retry-aware (R8/#10): `5xx` = transient (caller should retry),
279
+ * Status codes are retry-aware: `5xx` = transient (caller should retry),
152
280
  * `4xx` = terminal drop, `2xx` = accepted/benign-ignore.
153
281
  */
154
282
  async function processInbound(deps) {
@@ -173,17 +301,15 @@ async function processInbound(deps) {
173
301
  if (msg.decrypt_error) return new Response("undecryptable", { status: 422 });
174
302
  if (!msg.verified && !deps.acceptUnverified) return new Response("unverified sender signature", { status: 422 });
175
303
  if (deps.ignoreTypes.includes(msg.type)) {
176
- deps.seen?.add(messageId);
304
+ rememberSeen(deps.seen, messageId);
177
305
  return new Response("ignored type", { status: 202 });
178
306
  }
307
+ if (isX402Frame(msg.type)) return handleX402Inbound(deps, msg, messageId);
179
308
  const replyTarget = msg.group_handle ?? msg.sender_handle;
180
309
  if (!replyTarget) return new Response("no reply target", { status: 422 });
181
310
  const token = msg.group_handle ? encodeReplyToken(msg.conversation_id, replyTarget) : encodeReplyToken(msg.conversation_id, replyTarget, msg.id);
182
311
  await deps.client.markDelivered([msg.id]).catch(() => {});
183
- if (deps.seen) {
184
- if (deps.seen.size >= SEEN_CAP) deps.seen.clear();
185
- deps.seen.add(messageId);
186
- }
312
+ rememberSeen(deps.seen, messageId);
187
313
  const context = [senderContextLine(msg)];
188
314
  if (deps.threadContextTokenBudget != null) {
189
315
  const priorTurns = (await deps.client.thread(msg.conversation_id, { limit: THREAD_FETCH_LIMIT }).catch(() => [])).slice(0, -1);
@@ -198,9 +324,22 @@ async function processInbound(deps) {
198
324
  });
199
325
  return new Response("ok", { status: 200 });
200
326
  }
327
+ //#endregion
328
+ //#region src/channel-outbound.ts
329
+ /**
330
+ * OUTBOUND half of the pure rine channel pipeline — no `eve` import, so it is
331
+ * unit-testable with fake clients. `channel.ts` wires these to Eve's
332
+ * `message.completed` / failure events; the INBOUND half lives in `channel-core.ts`.
333
+ *
334
+ * On a terminal assistant message, reply back over rine IN-PLACE via the reply
335
+ * endpoint, preserving the inbound conversation; falls back to
336
+ * `parentConversationId` for legacy tokens. A session started by an x402 frame never
337
+ * prose-replies into the payment thread (the pay / fulfill tools transmit the signed
338
+ * frame in-thread instead) — the `x402` continuation-token flag short-circuits both.
339
+ */
201
340
  /**
202
341
  * Outbound for `message.completed`: every TERMINAL assistant message (any
203
- * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
342
+ * `finishReason` except the intermediate `tool-calls`) with text becomes a
204
343
  * reply. When the continuation token carries the inbound message id we reply
205
344
  * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
206
345
  * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
@@ -213,6 +352,7 @@ async function processCompletion(deps) {
213
352
  if (!deps.message) return;
214
353
  const rc = decodeReplyToken(deps.continuationToken);
215
354
  if (!rc) return;
355
+ if (rc.x402) return;
216
356
  if (rc.messageId) {
217
357
  await deps.client.reply(asMessageUuid(rc.messageId), { text: deps.message }, { type: deps.replyMessageType });
218
358
  return;
@@ -230,6 +370,7 @@ async function processCompletion(deps) {
230
370
  async function processFailure(continuationToken, client) {
231
371
  const rc = decodeReplyToken(continuationToken);
232
372
  if (!rc) return;
373
+ if (rc.x402) return;
233
374
  const text = "Sorry — I hit an internal error handling your request.";
234
375
  if (rc.messageId) {
235
376
  await client.reply(asMessageUuid(rc.messageId), { text }, { type: "rine.v1.error" });
@@ -245,14 +386,27 @@ async function processFailure(continuationToken, client) {
245
386
  const DEFAULT_INBOUND_PATH = "/rine/v1/inbound";
246
387
  const DEFAULT_REPLY_TYPE = "rine.v1.task_response";
247
388
  const DEFAULT_MAX_BODY_BYTES = 512 * 1024;
248
- /** Default token budget for the push-injected transcript (REQ-CTX-01, ≈8000 chars). */
389
+ /** Default token budget for the push-injected transcript (≈8000 chars). */
249
390
  const DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET = 2e3;
250
- /** Message types the connector itself emits — never auto-replied to (loop guard, R2). */
391
+ /**
392
+ * Message types never auto-replied to (loop guard): the connector's OWN
393
+ * outbound types (task_response/error/receipt), so two rine-eve agents don't
394
+ * auto-reply to each other forever.
395
+ *
396
+ * The three x402 payment frames are NOT here: v1.1 re-surfaces them as
397
+ * payment-aware agent turns, routed by `handleX402Inbound` with the prose
398
+ * auto-reply suppressed (the `x402` continuation-token flag), never by the generic
399
+ * assistant reply. Adding them here would revert to the v1.0 silent-drop behavior.
400
+ */
251
401
  const DEFAULT_IGNORE_TYPES = [
252
402
  "rine.v1.task_response",
253
403
  "rine.v1.error",
254
404
  "rine.v1.receipt"
255
405
  ];
406
+ /** Whether an env flag is truthy (`1` / `true`, case-insensitive). */
407
+ function envFlag(value) {
408
+ return value === "1" || value?.toLowerCase() === "true";
409
+ }
256
410
  /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
257
411
  function rineChannel(opts = {}) {
258
412
  const path = opts.path ?? process.env.RINE_INBOUND_PATH ?? DEFAULT_INBOUND_PATH;
@@ -261,6 +415,7 @@ function rineChannel(opts = {}) {
261
415
  const acceptUnverified = opts.acceptUnverified ?? false;
262
416
  const ignoreTypes = opts.ignoreTypes ?? DEFAULT_IGNORE_TYPES;
263
417
  const threadContextTokenBudget = opts.threadContextTokenBudget ?? DEFAULT_THREAD_CONTEXT_TOKEN_BUDGET;
418
+ const autoPay = opts.payments?.autoPay ?? envFlag(process.env.RINE_X402_AUTO_PAY);
264
419
  const seen = /* @__PURE__ */ new Set();
265
420
  const client = () => opts.client ?? getRineClient({
266
421
  agent: opts.agent ?? process.env.RINE_AGENT,
@@ -278,7 +433,8 @@ function rineChannel(opts = {}) {
278
433
  maxBodyBytes,
279
434
  ignoreTypes,
280
435
  seen,
281
- threadContextTokenBudget
436
+ threadContextTokenBudget,
437
+ autoPay
282
438
  }))],
283
439
  events: {
284
440
  "message.completed": async (data, channel) => {
@@ -301,4 +457,4 @@ function rineChannel(opts = {}) {
301
457
  });
302
458
  }
303
459
  //#endregion
304
- export { senderContextLine as a, messageIdFromWebhook as c, processInbound as i, processCompletion as n, decodeReplyToken as o, processFailure as r, encodeReplyToken as s, rineChannel as t };
460
+ export { processInbound as a, handleX402Inbound as c, x402GuidanceLine as d, decodeReplyToken as f, processFailure as i, isX402Frame as l, messageIdFromWebhook as m, rineChannel as n, senderContextLine as o, encodeReplyToken as p, processCompletion as r, X402_FRAME_TYPES as s, DEFAULT_IGNORE_TYPES as t, renderX402Body as u };
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Pure inbound/outbound pipeline for the rine channel — no `eve` import, so it is
3
- * unit-testable with fake clients/sends. `channel.ts` wires these to Eve's
4
- * `defineChannel` routes + events.
2
+ * INBOUND half of the pure rine channel pipeline — no `eve` import, so it is
3
+ * unit-testable with fake clients/sends. `channel.ts` wires this to Eve's
4
+ * `defineChannel` route; the OUTBOUND half (`processCompletion`/`processFailure`)
5
+ * lives in `channel-outbound.ts`.
5
6
  *
6
- * - INBOUND (`processInbound`): HMAC-verify (transport auth, R5) → parse → dedupe
7
+ * - INBOUND (`processInbound`): HMAC-verify (transport auth) → parse → dedupe
7
8
  * → `client.read(id)` to HPKE-decrypt + verify the sender's Ed25519 signature
8
- * (content auth, R5) → loop-guard → mark delivered → start/resume the session.
9
- * - OUTBOUND (`processCompletion`): on a terminal assistant message (R6) reply
10
- * back over rine IN-PLACE via the reply endpoint, preserving the inbound
11
- * conversation (R7); falls back to `parentConversationId` for legacy tokens.
9
+ * (content auth) → loop-guard → mark delivered → start/resume the session.
12
10
  *
13
11
  * TRUST NOTE: `verified === true` cryptographically binds the message's *signer*
14
12
  * (the envelope `kid`), but the SDK exposes only the server-asserted
@@ -42,41 +40,24 @@ export interface InboundDeps {
42
40
  maxBodyBytes: number;
43
41
  /** Inbound types to ignore (loop guard). */
44
42
  ignoreTypes: readonly string[];
43
+ /**
44
+ * Opt-in auto-pay (default OFF). When true, an inbound `x402_payment_required`
45
+ * at/below the policy's `autoPayThreshold` is paid with NO LLM turn — still
46
+ * bounded by the caps, deny-by-default, journal and reserve-lock. Any decline
47
+ * falls back to surfacing the quote to the model.
48
+ */
49
+ autoPay?: boolean;
45
50
  /** Bounded set of already-handled message ids (dedupe across retries/replays). */
46
51
  seen?: Set<string>;
47
52
  /**
48
53
  * Approximate token budget for the role-tagged transcript push-injected into the
49
- * session context (REQ-CTX-01). When unset, only the sender line is injected.
54
+ * session context. When unset, only the sender line is injected.
50
55
  */
51
56
  threadContextTokenBudget?: number;
52
57
  }
53
58
  /**
54
59
  * The inbound pipeline. Returns the HTTP {@link Response} the route should send.
55
- * Status codes are retry-aware (R8/#10): `5xx` = transient (caller should retry),
60
+ * Status codes are retry-aware: `5xx` = transient (caller should retry),
56
61
  * `4xx` = terminal drop, `2xx` = accepted/benign-ignore.
57
62
  */
58
63
  export declare function processInbound(deps: InboundDeps): Promise<Response>;
59
- export interface CompletionDeps {
60
- finishReason: string;
61
- message: string | null;
62
- continuationToken: string;
63
- client: AsyncRineClient;
64
- replyMessageType: string;
65
- }
66
- /**
67
- * Outbound for `message.completed`: every TERMINAL assistant message (any
68
- * `finishReason` except the intermediate `tool-calls`, R6) with text becomes a
69
- * reply. When the continuation token carries the inbound message id we reply
70
- * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
71
- * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
72
- * semantics are exactly as before. Legacy tokens (no message id) fall back to the
73
- * historical `send()+parentConversationId` path. No-ops for non-rine sessions
74
- * (token not ours) or empty messages.
75
- */
76
- export declare function processCompletion(deps: CompletionDeps): Promise<void>;
77
- /**
78
- * Best-effort error notice back to the sender on a terminal session failure.
79
- * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
80
- * type; falls back to `send()+parentConversationId` for legacy tokens.
81
- */
82
- export declare function processFailure(continuationToken: string, client: AsyncRineClient): Promise<void>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OUTBOUND half of the pure rine channel pipeline — no `eve` import, so it is
3
+ * unit-testable with fake clients. `channel.ts` wires these to Eve's
4
+ * `message.completed` / failure events; the INBOUND half lives in `channel-core.ts`.
5
+ *
6
+ * On a terminal assistant message, reply back over rine IN-PLACE via the reply
7
+ * endpoint, preserving the inbound conversation; falls back to
8
+ * `parentConversationId` for legacy tokens. A session started by an x402 frame never
9
+ * prose-replies into the payment thread (the pay / fulfill tools transmit the signed
10
+ * frame in-thread instead) — the `x402` continuation-token flag short-circuits both.
11
+ */
12
+ import type { AsyncRineClient } from "@rine-network/sdk";
13
+ export interface CompletionDeps {
14
+ finishReason: string;
15
+ message: string | null;
16
+ continuationToken: string;
17
+ client: AsyncRineClient;
18
+ replyMessageType: string;
19
+ }
20
+ /**
21
+ * Outbound for `message.completed`: every TERMINAL assistant message (any
22
+ * `finishReason` except the intermediate `tool-calls`) with text becomes a
23
+ * reply. When the continuation token carries the inbound message id we reply
24
+ * IN-PLACE via the reply endpoint (same conversation, no fork) keeping the
25
+ * connector's `replyMessageType` unchanged — type-preserving, so terminal/wake
26
+ * semantics are exactly as before. Legacy tokens (no message id) fall back to the
27
+ * historical `send()+parentConversationId` path. No-ops for non-rine sessions
28
+ * (token not ours) or empty messages.
29
+ */
30
+ export declare function processCompletion(deps: CompletionDeps): Promise<void>;
31
+ /**
32
+ * Best-effort error notice back to the sender on a terminal session failure.
33
+ * Replies IN-PLACE (reply endpoint, same conversation) keeping the `rine.v1.error`
34
+ * type; falls back to `send()+parentConversationId` for legacy tokens.
35
+ */
36
+ export declare function processFailure(continuationToken: string, client: AsyncRineClient): Promise<void>;
package/dist/channel.d.ts CHANGED
@@ -10,8 +10,21 @@
10
10
  * `channel-core.ts` (pure, testable); this file is the `defineChannel` wiring.
11
11
  */
12
12
  import type { AsyncRineClient } from "@rine-network/sdk";
13
- export { processInbound, processCompletion, processFailure, senderContextLine, } from "./channel-core.js";
14
- export type { ChannelSend, InboundDeps, CompletionDeps } from "./channel-core.js";
13
+ export { processInbound, senderContextLine } from "./channel-core.js";
14
+ export { processCompletion, processFailure } from "./channel-outbound.js";
15
+ export type { ChannelSend, InboundDeps } from "./channel-core.js";
16
+ export type { CompletionDeps } from "./channel-outbound.js";
17
+ /**
18
+ * Message types never auto-replied to (loop guard): the connector's OWN
19
+ * outbound types (task_response/error/receipt), so two rine-eve agents don't
20
+ * auto-reply to each other forever.
21
+ *
22
+ * The three x402 payment frames are NOT here: v1.1 re-surfaces them as
23
+ * payment-aware agent turns, routed by `handleX402Inbound` with the prose
24
+ * auto-reply suppressed (the `x402` continuation-token flag), never by the generic
25
+ * assistant reply. Adding them here would revert to the v1.0 silent-drop behavior.
26
+ */
27
+ export declare const DEFAULT_IGNORE_TYPES: string[];
15
28
  /** Options for {@link rineChannel}. All optional; identity defaults to env. */
16
29
  export interface RineChannelOptions {
17
30
  /** Acting agent (handle/name/UUID); defaults to `process.env.RINE_AGENT`. */
@@ -34,13 +47,23 @@ export interface RineChannelOptions {
34
47
  maxBodyBytes?: number;
35
48
  /**
36
49
  * Approximate token budget for the role-tagged recent-transcript push-injected
37
- * into the stateless Eve session context on each inbound (REQ-CTX-01). Older
50
+ * into the stateless Eve session context on each inbound. Older
38
51
  * turns are truncated first with a `[…earlier turns omitted]` marker; a
39
52
  * single-message thread injects no extra transcript. Default ~2000 tokens.
40
53
  */
41
54
  threadContextTokenBudget?: number;
42
55
  /** A pre-built client (tests / advanced use); otherwise env-resolved lazily. */
43
56
  client?: AsyncRineClient;
57
+ /**
58
+ * x402 payment behavior. `autoPay` is OPT-IN and DEFAULT OFF: when true, an
59
+ * inbound `x402_payment_required` at/below the policy's `autoPayThreshold` is paid
60
+ * with no LLM turn (still bounded by the caps, deny-by-default, journal and
61
+ * reserve-lock). Off ⇒ every quote is surfaced to the agent, which pays via
62
+ * `rine_pay`. Falls back to `RINE_X402_AUTO_PAY` (`1`/`true`) when unset.
63
+ */
64
+ payments?: {
65
+ autoPay?: boolean;
66
+ };
44
67
  }
45
68
  /** Build the rine channel. Default-export the result from `agent/channels/rine.ts`. */
46
69
  export declare function rineChannel(opts?: RineChannelOptions): import("eve/channels").Channel<undefined, Record<string, unknown>, Record<string, unknown>>;
package/dist/channel.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as senderContextLine, i as processInbound, n as processCompletion, r as processFailure, t as rineChannel } from "./channel-Bg8l58gg.js";
2
- export { processCompletion, processFailure, processInbound, rineChannel, senderContextLine };
1
+ import { a as processInbound, i as processFailure, n as rineChannel, o as senderContextLine, r as processCompletion, t as DEFAULT_IGNORE_TYPES } from "./channel-WlN3x1wP.js";
2
+ export { DEFAULT_IGNORE_TYPES, processCompletion, processFailure, processInbound, rineChannel, senderContextLine };
@@ -2,7 +2,7 @@ import { AsyncRineClient } from "@rine-network/sdk";
2
2
  import { resolveApiUrl, resolveConfigDir } from "@rine-network/core";
3
3
  //#region src/client.ts
4
4
  /**
5
- * Lazy `AsyncRineClient` construction (invariant R1: side-effect-free import).
5
+ * Lazy `AsyncRineClient` construction (side-effect-free import).
6
6
  *
7
7
  * No client is built at module load. The SDK client is created the first time a
8
8
  * tool's `execute` (or a channel handler) actually fires, then memoized per
@@ -24,7 +24,7 @@ function cacheKey(configDir, apiUrl) {
24
24
  /**
25
25
  * Get (or lazily build + memoize) the shared `AsyncRineClient` for `opts`,
26
26
  * already scoped to `opts.agent` when present. Call ONLY from inside a tool
27
- * `execute` / a channel handler — never at module top level (R1).
27
+ * `execute` / a channel handler — never at module top level.
28
28
  */
29
29
  function getRineClient(opts = {}) {
30
30
  const configDir = opts.configDir ?? resolveConfigDir();
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Lazy `AsyncRineClient` construction (invariant R1: side-effect-free import).
2
+ * Lazy `AsyncRineClient` construction (side-effect-free import).
3
3
  *
4
4
  * No client is built at module load. The SDK client is created the first time a
5
5
  * tool's `execute` (or a channel handler) actually fires, then memoized per
@@ -27,7 +27,7 @@ export interface RineClientOpts {
27
27
  /**
28
28
  * Get (or lazily build + memoize) the shared `AsyncRineClient` for `opts`,
29
29
  * already scoped to `opts.agent` when present. Call ONLY from inside a tool
30
- * `execute` / a channel handler — never at module top level (R1).
30
+ * `execute` / a channel handler — never at module top level.
31
31
  */
32
32
  export declare function getRineClient(opts?: RineClientOpts): AsyncRineClient;
33
33
  /** Test-only: drop all memoized clients (not part of the public surface). */
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `formatError(err)` — invariant R2: turn any thrown SDK error into a readable
2
+ * `formatError(err)` — turns any thrown SDK error into a readable
3
3
  * string for the LLM, never a stack trace. Tools wrap their one `await client.*`
4
4
  * call in `try/catch → formatError` and RESOLVE (never reject) for mapped errors.
5
5
  *
package/dist/format.d.ts CHANGED
@@ -2,12 +2,12 @@
2
2
  * Pure render functions: turn SDK return values into the human-readable strings
3
3
  * tools + the channel hand to the LLM / send back over rine.
4
4
  *
5
- * Invariant R4: these read ONLY `plaintext` / `decrypt_error` / verification
5
+ * These read ONLY `plaintext` / `decrypt_error` / verification
6
6
  * fields. They NEVER read `encrypted_payload` or any envelope/ciphertext field,
7
7
  * so ciphertext can never reach the LLM context through a rendered string.
8
8
  */
9
9
  import type { ThreadEntry } from "@rine-network/sdk";
10
- import type { AgentProfile, AgentSummary, DecryptedMessage, GroupRead } from "./types.js";
10
+ import type { AgentProfile, AgentSummary, DecryptedMessage, GroupRead, JoinRequestRead, JoinResult } from "./types.js";
11
11
  /** One thread turn as a role-tagged line: `[sent] you: …` / `[received] alice@org: …`. */
12
12
  export declare function renderThreadLine(e: ThreadEntry): string;
13
13
  /**
@@ -41,11 +41,15 @@ export declare function renderProfile(p: AgentProfile): string;
41
41
  /**
42
42
  * Self-diagnose a group's E2EE mode. Uses `mls_group_id !== null`, OR the
43
43
  * explicit `mls_enabled`/`mls_pending` flags. With MLS support present this is a
44
- * CAPABILITY flag, not a failure flag (R9).
44
+ * CAPABILITY flag, not a failure flag.
45
45
  */
46
46
  export declare function groupIsMls(g: GroupRead): boolean;
47
47
  /**
48
48
  * A group rendered for `rine_group_inspect`. Both the MLS and sender-key branches
49
- * are `[OK]` — an MLS group is readable/postable from here (R9).
49
+ * are `[OK]` — an MLS group is readable/postable from here.
50
50
  */
51
51
  export declare function renderGroup(g: GroupRead): string;
52
+ /** `rine_group_join` outcome: immediate membership vs a pending vote. */
53
+ export declare function renderJoinResult(target: string, result: JoinResult): string;
54
+ /** A numbered list of the caller's pending group invites, or the empty-state line. */
55
+ export declare function renderInvites(items: readonly JoinRequestRead[]): string;