@rine-network/openclaw 0.4.0 → 0.6.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.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { i as readRineCredentials, n as buildRineClient, o as resolveRineConfigFromCfg, r as sendRineReply, s as INTERNAL_TOOLS, t as rinePlugin } from "./channel-DqxESY90.js";
2
- import { i as normalizeStandardWebhook, n as normalizeA2A, t as isAllowed } from "./inbound-DEe69mNL.js";
1
+ import { a as readRineCredentials, c as INTERNAL_TOOLS, i as sendRineText, n as buildRineClient, r as sendRineReply, s as resolveRineConfigFromCfg, t as rinePlugin } from "./channel-DiPLuJsk.js";
2
+ import { i as normalizeA2A, n as sleep, o as normalizeStandardWebhook, r as isAllowed } from "./backoff-a3Dly8X4.js";
3
3
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
4
- import { fetchAgents, resolveAgent } from "@rine-network/core";
5
- import { tools } from "@rine-network/mcp/tools";
4
+ import { ACTOR_SURFACES, resolveActor } from "@rine-network/core";
5
+ import { actorKeysFor, namesActor, tools } from "@rine-network/mcp/tools";
6
6
  import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/channel-inbound";
7
7
  import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
8
8
  //#region openclaw.plugin.json
@@ -35,7 +35,7 @@ var configSchema = {
35
35
  },
36
36
  "agentId": {
37
37
  "type": "string",
38
- "description": "rine agent id to bind (default: credentialed agent)."
38
+ "description": "The rine agent this install acts as whose inbox the notify service reads, and the acting agent defaulted onto every tool call that takes one. Name, handle, or UUID. Unset, the RINE_AGENT environment variable is used, then the org's only agent; an org with more than one and neither set idles the notify service."
39
39
  },
40
40
  "baseUrl": {
41
41
  "type": "string",
@@ -44,7 +44,7 @@ var configSchema = {
44
44
  "pollIntervalMs": {
45
45
  "type": "number",
46
46
  "default": 6e4,
47
- "description": "POLL only: interval between /poll checks."
47
+ "description": "Interval between /poll checks on the POLL transport, and how often an install without credentials re-checks for them on any transport."
48
48
  },
49
49
  "reconnectBaseMs": {
50
50
  "type": "number",
@@ -150,44 +150,84 @@ function makeOnMessage(deps) {
150
150
  //#endregion
151
151
  //#region src/runtime-dispatch.ts
152
152
  /**
153
- * The Funnel relay self-sends this type (`from == to`); it is a one-way event the agent ACTS
154
- * on and must never auto-reply to (the reply is a self-send the server rejects). Compared as a
155
- * literal so the guard never depends on the installed SDK exporting a named constant — mirrors
156
- * the rine-hermes `_FUNNEL_WEBHOOK_TYPE` precedent.
157
- */
158
- const WEBHOOK_TYPE = "rine.v1.webhook";
159
- /**
160
- * A one-way inbound: a Funnel webhook, or any self-addressed message (`from == to`). The agent
161
- * wakes to act via tools, but an auto-reply would target itself and be rejected.
153
+ * Types the agent still WAKES for it acts on them through its tools but must never
154
+ * auto-reply to, each mapped to the reason the operator reads in the log. Compared as
155
+ * literals so a guard never depends on the installed SDK exporting a named constant
156
+ * (mirrors the rine-hermes `_FUNNEL_WEBHOOK_TYPE` precedent).
157
+ *
158
+ * The Funnel webhook is relayed as the agent itself (`from == to`), so a reply targets the
159
+ * agent and the server rejects it. The three x402 frames carry a payment handshake, and a
160
+ * canned text reply injected into that thread corrupts it. The sender-key distribution and
161
+ * group-invite envelopes are control frames whose payload the recipient parses as JSON:
162
+ * a text answer to one is prose the peer's ingest path has to throw away, and because
163
+ * `rine_reply` mirrors the inbound type the answer also goes on the wire wearing the
164
+ * control frame's own label.
162
165
  */
163
- function isOneWayEvent(msg) {
164
- return msg.type === WEBHOOK_TYPE || msg.selfSend === true;
166
+ const NO_AUTO_REPLY_TYPES = new Map([
167
+ ["rine.v1.webhook", "one-way Funnel webhook act via tools; a reply would target self"],
168
+ ["rine.v1.x402_payment_required", "x402 frame — act via the rine_pay tool; a text reply would corrupt the payment handshake"],
169
+ ["rine.v1.x402_payment", "x402 frame — act via the rine_pay tool; a text reply would corrupt the payment handshake"],
170
+ ["rine.v1.x402_receipt", "x402 frame — act via the rine_pay tool; a text reply would corrupt the payment handshake"],
171
+ ["rine.v1.sender_key_distribution", "sender-key control frame — the peer parses it as JSON, and a text answer would be typed as one"],
172
+ ["rine.v1.group_invite", "group-invite control frame — the peer parses it as JSON, and a text answer would be typed as one"]
173
+ ]);
174
+ /** Why this inbound must not be auto-answered, or undefined when an answer is correct. */
175
+ function noAutoReplyReason(msg, opts) {
176
+ if (msg.selfSend === true) return "self-addressed message — a reply would target self";
177
+ const byType = NO_AUTO_REPLY_TYPES.get(msg.type);
178
+ if (byType) return byType;
179
+ if (opts?.replayed) return "catch-up/replayed frame — live-phase frames only";
165
180
  }
166
181
  /**
167
- * The three x402 payment frames: the 402 quote, the signed authorization, and the settlement
168
- * receipt. Like a one-way event the agent still wakes to ACT (pay via the `rine_pay` tool, read
169
- * the receipt), but a canned TEXT auto-reply would be injected into the payment thread and
170
- * corrupt the x402 handshake. Compared as literals so the guard never depends on the installed
171
- * core SDK exporting `X402_MESSAGE_TYPE` mirrors the WEBHOOK_TYPE precedent above.
182
+ * How an answer to `msg` reaches its audience, or undefined when there is no route.
183
+ *
184
+ * A GROUP is answered by SENDING to the group, never by replying. `POST /messages/{id}/reply`
185
+ * routes to the "other party" of a 1:1 pair and a group row carries `to_agent_id IS NULL` —
186
+ * there is no other party, so the lifted `rine_reply` handler refuses one GET earlier
187
+ * ("Cannot reply to a group post — it has no single recipient. Answer the group by sending
188
+ * to " and then ONE OF TWO tails — `rine-mcp/src/tools/messaging.ts`,
189
+ * `replyToGroupRefusal`) and the route behind it refuses too. The tail is the group's OWN
190
+ * handle when the message it just fetched carries one, and otherwise "its own handle: " plus
191
+ * how such a handle is spelled — `GROUP_HANDLE_PARTS`, rine-core's shared clause in
192
+ * `handle-words.ts`, composed rather than retyped. Both tails keep the advice; the second is
193
+ * for the row whose group has no handle to echo, which is the SAME missing handle
194
+ * `answerRouteOf` below returns no route for. Its sibling refusal, "Cannot determine
195
+ * recipient from message", is the NON-group case: a 1:1 row with no `to_agent_id` at all.
196
+ *
197
+ * `isGroup` is true whenever the inbound carries a group handle OR a bare group id, so a
198
+ * group frame can arrive with no handle (a webhook body carrying only `group_id`). There is
199
+ * no route in that case: a group UUID typechecks as a send target and is then encrypted 1:1
200
+ * and rejected server-side, so refusing loudly beats sending something undeliverable.
172
201
  */
173
- const X402_FRAME_TYPES = new Set([
174
- "rine.v1.x402_payment_required",
175
- "rine.v1.x402_payment",
176
- "rine.v1.x402_receipt"
177
- ]);
178
- function isX402Frame(msg) {
179
- return X402_FRAME_TYPES.has(msg.type);
202
+ function answerRouteOf(msg) {
203
+ if (!msg.isGroup) return {
204
+ kind: "direct",
205
+ handle: msg.fromHandle
206
+ };
207
+ return msg.groupHandle ? {
208
+ kind: "group",
209
+ handle: msg.groupHandle
210
+ } : void 0;
180
211
  }
181
212
  /**
182
213
  * The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
183
214
  *
184
- * The body also binds the reply target to the SENDER. Without this, a model that replies via the
185
- * optional `rine_send` tool can copy the message's own `to_agent_id` (= this agent's id, surfaced
186
- * by `rine_read`) and self-target, producing `Unknown target`. The auto-routing path is
187
- * `rine_reply message_id=<id>`, which derives the recipient from the original sender server-side.
215
+ * The body names only tools this plugin exposes. It used to point at `rine_reply`, which is
216
+ * an INTERNAL tool the host never registers, so every inbound spent a turn on tool calls the
217
+ * runtime answered with `Tool rine_reply not found`. The plugin routes the turn's text out on
218
+ * its own, so the correct instruction is to answer and call nothing.
219
+ *
220
+ * The body also states the audience. On a group post the old text named the poster twice and
221
+ * the group never, i.e. it instructed the model to answer a group by privately DM'ing whoever
222
+ * spoke last. `route` is resolved once by the dispatcher and passed in, so what the model is
223
+ * told matches what `deliver` actually does — absent when the turn is not delivered at all.
188
224
  */
189
- function pointerText(msg) {
190
- return `[rine ${msg.type} from ${msg.fromHandle}] message ${msg.id} (read with rine_read; reply with rine_reply message_id=${msg.id} — it auto-routes to ${msg.fromHandle}. If you use rine_send instead, set to=${msg.fromHandle}, never your own agent id.)`;
225
+ function pointerText(msg, route) {
226
+ const inGroup = msg.isGroup && msg.groupHandle ? ` in group ${msg.groupHandle}` : "";
227
+ const head = `[rine ${msg.type} from ${msg.fromHandle}${inGroup}] message ${msg.id}`;
228
+ if (!route) return `${head} (read it with rine_read. Nothing you write this turn is sent — act on this message with your tools instead.)`;
229
+ if (route.kind === "group") return `${head} (read it with rine_read, then answer in your turn — your answer is posted to the whole group ${route.handle} automatically. Do not call a send tool, and never answer the sender privately.)`;
230
+ return `${head} (read it with rine_read, then answer in your turn — your answer is delivered back to ${route.handle} automatically. Do not call a send tool to answer.)`;
191
231
  }
192
232
  /**
193
233
  * Production dispatch fn: wakes an agent turn for one inbound rine message via the
@@ -203,6 +243,8 @@ function makeRuntimeDispatcher(params) {
203
243
  signal.throwIfAborted();
204
244
  const peerId = msg.isGroup ? msg.conversationId : msg.fromHandle;
205
245
  const timestamp = msg.createdAt ? Date.parse(msg.createdAt) : void 0;
246
+ const suppressed = noAutoReplyReason(msg, opts);
247
+ const route = suppressed ? void 0 : answerRouteOf(msg);
206
248
  let dispatchErr;
207
249
  await dispatchInboundDirectDmWithRuntime({
208
250
  cfg,
@@ -218,7 +260,7 @@ function makeRuntimeDispatcher(params) {
218
260
  senderAddress: msg.fromHandle,
219
261
  recipientAddress: agentId,
220
262
  conversationLabel: msg.isGroup ? `rine group ${msg.conversationId}` : msg.fromHandle,
221
- rawBody: pointerText(msg),
263
+ rawBody: pointerText(msg, route),
222
264
  messageId: msg.id,
223
265
  timestamp: Number.isFinite(timestamp) ? timestamp : void 0,
224
266
  deliver: async (payload) => {
@@ -227,19 +269,15 @@ function makeRuntimeDispatcher(params) {
227
269
  logger.warn(`rine: dropping non-text reply for ${msg.id} (rine replies are text-only)`);
228
270
  return;
229
271
  }
230
- if (isOneWayEvent(msg)) {
231
- logger.debug?.(`rine: no auto-reply for one-way ${msg.type} ${msg.id} (act via tools; reply would target self)`);
272
+ if (suppressed) {
273
+ logger.debug?.(`rine: no auto-reply for ${msg.type} ${msg.id} (${suppressed})`);
232
274
  return;
233
275
  }
234
- if (isX402Frame(msg)) {
235
- logger.debug?.(`rine: no auto-reply for x402 frame ${msg.type} ${msg.id} (act via the rine_pay tool; a text reply would corrupt the payment handshake)`);
276
+ if (!route) {
277
+ logger.error(`rine: cannot answer group message ${msg.id} inbound carries no group handle`);
236
278
  return;
237
279
  }
238
- if (opts?.replayed) {
239
- logger.debug?.(`rine: no auto-reply for catch-up/replayed ${msg.id} (live-phase frames only)`);
240
- return;
241
- }
242
- await replyToRine(client, msg, text, logger);
280
+ await deliverAnswer(client, msg, route, text, logger, agentId);
243
281
  },
244
282
  onRecordError: (err) => logger.warn(`rine: session-record error for ${msg.id}: ${msgOf(err)}`),
245
283
  onDispatchError: (err) => {
@@ -249,9 +287,19 @@ function makeRuntimeDispatcher(params) {
249
287
  if (dispatchErr) throw dispatchErr;
250
288
  };
251
289
  }
252
- async function replyToRine(client, msg, text, logger) {
253
- if (!text) return;
290
+ /**
291
+ * Route one agent turn back out to its audience. A group answer is a broadcast through the
292
+ * lifted `rine_send` handler — the same call `channel.ts`'s outbound adapter already makes,
293
+ * so no crypto or HTTP is reimplemented and the auto-answer does not become allowlist-gated
294
+ * (the handler is invoked directly, never offered to the model). A 1:1 answer keeps the
295
+ * reply-in-place path, which preserves the conversation.
296
+ */
297
+ async function deliverAnswer(client, msg, route, text, logger, agentId) {
254
298
  try {
299
+ if (route.kind === "group") {
300
+ await sendRineText(client, route.handle, text, agentId);
301
+ return;
302
+ }
255
303
  await sendRineReply(client, msg, text);
256
304
  } catch (err) {
257
305
  logger.error(`rine: outbound reply failed for ${msg.id}: ${msgOf(err)}`);
@@ -387,7 +435,7 @@ async function deleteWebhook(client, webhookId, logger) {
387
435
  }
388
436
  }
389
437
  async function fallbackToSse(tc) {
390
- const { runSseTransport } = await import("./sse-CL8OTrkL.js");
438
+ const { runSseTransport } = await import("./sse-jRmQV4GZ.js");
391
439
  await runSseTransport(tc);
392
440
  }
393
441
  function waitUntilAbort(signal) {
@@ -406,20 +454,23 @@ const SERVICE_ID = "rine-notify";
406
454
  async function runTransport(tc) {
407
455
  switch (tc.config.transport) {
408
456
  case "poll": {
409
- const { runPollTransport } = await import("./poll-DKsUxQpG.js");
457
+ const { runPollTransport } = await import("./poll-BWvVevTN.js");
410
458
  return runPollTransport(tc);
411
459
  }
412
460
  case "expose": return runExposeTransport(tc);
413
461
  default: {
414
- const { runSseTransport } = await import("./sse-CL8OTrkL.js");
462
+ const { runSseTransport } = await import("./sse-jRmQV4GZ.js");
415
463
  return runSseTransport(tc);
416
464
  }
417
465
  }
418
466
  }
419
467
  /**
420
- * Resolve the rine agent id this install is bound to. `channels.rine.agentId` (a UUID,
421
- * handle, or bare name) wins; otherwise the org's sole agent is auto-selected — the same
422
- * resolution the CLI uses (`fetchAgents` + `resolveAgent`).
468
+ * Resolve the UUID of the agent whose inbox the notify service reads. This is the
469
+ * *transport* half of `channels.rine.agentId` the tool half never comes here, because
470
+ * `withActingAgent` hands rine-mcp the operator's string unresolved and it resolves once
471
+ * per call. `channels.rine.agentId` (a UUID, handle, or bare name) wins, then `RINE_AGENT`,
472
+ * then the org's sole agent — one ladder, `resolveActor`, the same one every other surface
473
+ * climbs. Configuration beating the environment is precedence and stays silent.
423
474
  *
424
475
  * The OAuth `client_id` is NOT an agent id: credentials.json stores `client_id`/`client_secret`,
425
476
  * and binding the transport to it requests `/agents/{client_id}/stream|messages`, which 404s on
@@ -427,10 +478,28 @@ async function runTransport(tc) {
427
478
  * multi-agent org with no `agentId` set — both surface an actionable log; the gateway
428
479
  * health-monitor restarts the idle service, re-resolving once connectivity/config is fixed.
429
480
  */
481
+ /**
482
+ * How the TRANSPORT half spells the acting agent in a sentence it prints.
483
+ *
484
+ * `ACTOR_SURFACES.mcp` with the spelling replaced, on the precedent the core's own
485
+ * `ACTOR_SURFACES` sets (`send --from` is `cli` with `alias` overridden — spread it,
486
+ * do not mint a fourth entry): there is no `agent` parameter anywhere on this path.
487
+ * The value came out of `openclaw.json`, so a warning telling the operator to "omit
488
+ * agent" names a lever this surface does not have, and the one it does have —
489
+ * `channels.rine.agentId` — is the one the catch branch below already names. No
490
+ * `alias`: this surface never had a retired spelling, and it passes one value.
491
+ */
492
+ const TRANSPORT_SURFACE = {
493
+ ...ACTOR_SURFACES.mcp,
494
+ alias: void 0,
495
+ canonical: "channels.rine.agentId",
496
+ flagHint: "channels.rine.agentId"
497
+ };
430
498
  async function resolveBoundAgentId(client, config, logger) {
431
499
  try {
432
- const agents = await fetchAgents(client.client);
433
- return await resolveAgent(client.apiUrl, agents, config.agentId);
500
+ const resolved = await resolveActor(client.client, config.agentId, void 0, TRANSPORT_SURFACE);
501
+ if (resolved.warning !== void 0) logger.warn(`rine: ${resolved.warning}`);
502
+ return resolved.agentId;
434
503
  } catch (err) {
435
504
  logger.error(`rine: cannot resolve agent to bind (${err instanceof Error ? err.message : String(err)}). Set channels.rine.agentId (UUID or handle) or check connectivity — notify service idle.`);
436
505
  return;
@@ -467,8 +536,57 @@ async function startNotifyLoop(params) {
467
536
  await runTransport(tc);
468
537
  }
469
538
  /**
539
+ * Whether `creds` carry everything the chosen transport needs. Onboarding writes
540
+ * credentials.json in two steps — the OAuth entry first, `poll_url` seconds later once the
541
+ * agent exists — so a POLL install waits for the second write. Picking credentials up in
542
+ * between would freeze `pollUrl: undefined` into the transport context, and the poll loop
543
+ * exits for good on a missing poll_url; it is also the bottom rung of the SSE fallback.
544
+ */
545
+ function isTransportReady(creds, config) {
546
+ if (!creds.entry) return false;
547
+ return config.transport !== "poll" || Boolean(creds.pollUrl);
548
+ }
549
+ /**
550
+ * Wait for rine credentials to appear, re-reading the config dir on the poll cadence.
551
+ * A fresh install registers itself through `rine_onboard` mid-session; picking the new
552
+ * credentials up here brings the inbox live without a gateway restart. Resolves undefined
553
+ * when `stop()` aborts the wait.
554
+ */
555
+ async function waitForCredentials(config, signal) {
556
+ while (!signal.aborted) {
557
+ try {
558
+ await sleep(config.pollIntervalMs, signal);
559
+ } catch {
560
+ return;
561
+ }
562
+ const creds = readRineCredentials(config);
563
+ if (isTransportReady(creds, config)) return creds;
564
+ }
565
+ }
566
+ /** Resolve credentials (waiting for them if the install has not onboarded), then run the loop. */
567
+ async function startWhenCredentialed(params) {
568
+ const { api, config, signal, logger } = params;
569
+ let creds = readRineCredentials(config);
570
+ if (!creds.entry) {
571
+ logger.warn(`rine: no credentials.json at ${creds.configDir} — notify service idle until credentials appear (run rine_onboard or set RINE_CONFIG_DIR)`);
572
+ const found = await waitForCredentials(config, signal);
573
+ if (!found) return;
574
+ creds = found;
575
+ logger.info(`rine: credentials found at ${creds.configDir} — starting notify service`);
576
+ }
577
+ await startNotifyLoop({
578
+ api,
579
+ config,
580
+ creds,
581
+ client: buildRineClient(creds),
582
+ signal,
583
+ logger
584
+ });
585
+ }
586
+ /**
470
587
  * The single background notify service. `start(ctx)` resolves creds, owns the
471
- * AbortController (the service ctx has NO abort signal — see SDK_CONTRACT.md), then
588
+ * AbortController OpenClaw hands a service context that carries no abort signal, so
589
+ * cancellation is ours to construct and ours to fire — then
472
590
  * hands off to `startNotifyLoop` which resolves the bound agent id and runs the transport.
473
591
  * `stop` aborts it. The long-lived loop lives here, not in a channel `gateway.startAccount`.
474
592
  */
@@ -479,22 +597,15 @@ function makeRineService(api) {
479
597
  start(ctx) {
480
598
  const logger = ctx.logger ?? api.logger;
481
599
  const config = resolveRineConfigFromCfg(api.config);
482
- const creds = readRineCredentials(config);
483
- if (!creds.entry) {
484
- logger.warn(`rine: no credentials.json at ${creds.configDir} — notify service idle (run rine_onboard or set RINE_CONFIG_DIR)`);
485
- return;
486
- }
487
- const client = buildRineClient(creds);
488
600
  controller = new AbortController();
489
601
  const signal = controller.signal;
490
- startNotifyLoop({
602
+ startWhenCredentialed({
491
603
  api,
492
604
  config,
493
- creds,
494
- client,
495
605
  signal,
496
606
  logger
497
607
  }).catch((err) => {
608
+ if (signal.aborted) return;
498
609
  logger.error(`rine: notify transport exited: ${err instanceof Error ? err.message : String(err)}`);
499
610
  });
500
611
  },
@@ -505,12 +616,116 @@ function makeRineService(api) {
505
616
  };
506
617
  }
507
618
  //#endregion
619
+ //#region src/tool-actor.ts
620
+ /**
621
+ * The exposed tools that take an acting agent.
622
+ *
623
+ * A set rather than a per-tool spelling: rine-mcp spells the concept `agent` on every one
624
+ * of them, sends included, so the map's values had collapsed to one word and the only
625
+ * thing left to record is membership. The retired spellings (`as`, and `from` on the two
626
+ * sends) are still *read* by rine-mcp, but nothing here writes them — a wrapper that
627
+ * defaulted a retired word would put it back in front of the model.
628
+ *
629
+ * A tool absent from this set takes no acting agent at all. Where such a tool has an
630
+ * `agent`-shaped argument it means something else: `rine_inspect`'s `agent` is the
631
+ * agent being looked up, `rine_onboard`'s `agent_name` names the agent being created,
632
+ * and `rine_pay`/`rine_fulfill` derive the payee from the quote's own addressee.
633
+ *
634
+ * `rine_group_invite` is the tool the rename moved. Its `agent` used to be the
635
+ * **invitee** and is now the actor, the invitee having taken its own `invitee` /
636
+ * `invitees` slots. No presence check can tell those two readings apart — the property
637
+ * exists either way — so the pair is pinned against the shipped schema by test, and
638
+ * what makes that test meaningful is `invitee` existing at all: the invitee has
639
+ * nowhere else to live. Swapping them would invite the installation's own agent and
640
+ * report success.
641
+ */
642
+ const ACTOR_TOOLS = new Set([
643
+ "rine_groups",
644
+ "rine_group_create",
645
+ "rine_group_join",
646
+ "rine_group_roster",
647
+ "rine_group_inspect",
648
+ "rine_group_invite",
649
+ "rine_group_invites",
650
+ "rine_group_requests",
651
+ "rine_group_vote",
652
+ "rine_group_leave",
653
+ "rine_group_remove",
654
+ "rine_group_sync",
655
+ "rine_group_reclaim",
656
+ "rine_send",
657
+ "rine_send_and_wait",
658
+ "rine_read",
659
+ "rine_inbox",
660
+ "rine_thread"
661
+ ]);
662
+ /** The one word this plugin writes the acting agent into, as rine-mcp declares it. */
663
+ const ACTOR_KEY = ACTOR_SURFACES.mcp.canonical;
664
+ /** Throw on a tool in {@link ACTOR_TOOLS} that no longer declares `agent`. */
665
+ function assertDeclaresActor(def) {
666
+ const props = def.inputSchema.properties;
667
+ if (props === void 0 || !(ACTOR_KEY in props)) throw new Error(`rine: mcp tool "${def.name}" no longer declares its acting-agent parameter "${ACTOR_KEY}" — version skew`);
668
+ }
669
+ /**
670
+ * Bind the acting agent this install is configured for (`channels.rine.agentId`) as the
671
+ * default of the tool's own acting-agent argument. Without it the plugin emits
672
+ * `X-Rine-Agent` on no tool call at all, so a host running several agents acts as
673
+ * whichever one the org's single-agent shortcut picks — or fails to resolve one.
674
+ *
675
+ * The operator's string travels **unresolved**: rine-mcp accepts a name, handle or UUID
676
+ * and resolves it per call. This is the tool-facing half of `channels.rine.agentId`, and
677
+ * it is not the same object as the notify service's binding — `resolveBoundAgentId`
678
+ * turns the same setting into one UUID, up front, for the inbox the transports read.
679
+ *
680
+ * A model-supplied value still wins, so the operator's binding is a default and not a
681
+ * cap — but an empty string names nobody, and letting one through would discard the
682
+ * binding and hand the choice straight back to the server. 🔴 "Did the model name one?"
683
+ * is rine-mcp's question to answer, not this file's: it reads retired spellings this
684
+ * plugin never writes, and defaulting `agent` beside a model-supplied `from` would turn
685
+ * one caller's choice into a two-values refusal. Configuration is a lower rung than the
686
+ * call, and a lower rung loses silently.
687
+ *
688
+ * Mirrors {@link withAutoPayDefault}: one argument re-defaulted, nothing else touched. A
689
+ * tool with no acting-agent argument is returned unchanged, and a tool whose argument has
690
+ * disappeared upstream throws at load rather than silently dropping the actor — the
691
+ * same version-skew posture as `requireTool`.
692
+ */
693
+ function withActingAgent(def, agentId) {
694
+ if (agentId === void 0) return def;
695
+ if (!ACTOR_TOOLS.has(def.name)) return def;
696
+ assertDeclaresActor(def);
697
+ return {
698
+ ...def,
699
+ handler: (ctx, params) => {
700
+ const args = { ...params ?? {} };
701
+ if (!namesActor(args, actorKeysFor(def.name))) args[ACTOR_KEY] = agentId;
702
+ return def.handler(ctx, args);
703
+ }
704
+ };
705
+ }
706
+ //#endregion
508
707
  //#region src/tools.ts
509
708
  /** Tools exposed by the plugin (filtered from the mcp tool array). */
510
709
  const EXPOSED_TOOLS = [
511
710
  "rine_whoami",
512
711
  "rine_discover",
712
+ "rine_inspect",
713
+ "rine_discover_groups",
714
+ "rine_groups",
715
+ "rine_group_create",
716
+ "rine_group_join",
717
+ "rine_group_roster",
718
+ "rine_group_inspect",
719
+ "rine_group_invite",
720
+ "rine_group_invites",
721
+ "rine_group_requests",
722
+ "rine_group_vote",
723
+ "rine_group_leave",
724
+ "rine_group_remove",
725
+ "rine_group_sync",
726
+ "rine_group_reclaim",
513
727
  "rine_send",
728
+ "rine_send_and_wait",
514
729
  "rine_read",
515
730
  "rine_inbox",
516
731
  "rine_thread",
@@ -518,12 +733,52 @@ const EXPOSED_TOOLS = [
518
733
  "rine_pay",
519
734
  "rine_fulfill"
520
735
  ];
521
- /** Mutating tools gated behind the allowlist (manifest `toolMetadata.optional`). */
736
+ /**
737
+ * Tools with an irreversible external effect ON A THIRD PARTY, gated behind the operator's
738
+ * tool allowlist. The manifest's `toolMetadata.optional` carries the same set — OpenClaw ORs
739
+ * the two, so both must agree. `rine_send` and `rine_send_and_wait` speak to a third party
740
+ * over an encrypted channel — the second one is a send like any other, and the reply it waits
741
+ * for does not make the message it already delivered take-backable;
742
+ * `rine_pay` moves funds and `rine_fulfill` settles on-chain.
743
+ *
744
+ * `rine_group_invite`, `rine_group_vote`, `rine_group_remove` and `rine_group_reclaim` join
745
+ * them for the same reason. An invite acts on the group's roster on membership alone: the
746
+ * server authorises it with read access rather than admin on every policy but `closed`. On
747
+ * a closed group it hands the named agent a seat it can redeem straight away, together with
748
+ * a ratchet-tree leaf and Welcome minted inside the same call; on a majority or unanimity
749
+ * group it files a join request in the group's name and spends this host's own approval on
750
+ * it, and the electorate decides. An approve that crosses a group's threshold hands a
751
+ * stranger the group's keys and cannot be taken back. A remove evicts somebody else — on an
752
+ * MLS group it posts a Remove commit that takes their ratchet-tree leaf and that every
753
+ * member downloads at O(N). Reclamation posts one Remove commit per orphaned leaf, and an
754
+ * agent whose invitation lapsed and who is re-invited between the tree read and the commit
755
+ * loses the fresh leaf with them.
756
+ *
757
+ * Note the asymmetry with the ungated `rine_group_sync`, whose expensive rung also costs
758
+ * the group O(N): the criterion is an irreversible effect on a THIRD PARTY, not cost.
759
+ *
760
+ * Every other group verb is offered under the default empty allowlist, on the same reasoning
761
+ * that keeps `rine_onboard` out of this set: gating a tool the plugin's core function depends
762
+ * on leaves a fresh agent unable to work. An agent that cannot create, discover, join or read
763
+ * a group is not a group participant at all, and an operator would have to edit the allowlist
764
+ * before the plugin did anything with groups. Note the asymmetry with `rine_group_leave`,
765
+ * which is irreversible but acts only on THIS host: gating it would leave an agent unable to
766
+ * get out of a group somebody else put it in.
767
+ *
768
+ * `rine_onboard` is deliberately absent: registering the org is the first thing an
769
+ * uncredentialed install does, and on an install that already holds credentials the tool
770
+ * returns the existing org and writes nothing. Gating it leaves a fresh agent unable to
771
+ * register itself.
772
+ */
522
773
  const OPTIONAL_TOOLS = new Set([
523
774
  "rine_send",
524
- "rine_onboard",
775
+ "rine_send_and_wait",
525
776
  "rine_pay",
526
- "rine_fulfill"
777
+ "rine_fulfill",
778
+ "rine_group_invite",
779
+ "rine_group_vote",
780
+ "rine_group_remove",
781
+ "rine_group_reclaim"
527
782
  ]);
528
783
  /**
529
784
  * Payee tools exposed only when the installed mcp actually provides them. `rine_fulfill`
@@ -531,6 +786,12 @@ const OPTIONAL_TOOLS = new Set([
531
786
  * `^`-range that includes it, so real installs always expose it. This lenience only covers
532
787
  * the version window where a locally-linked mcp predates the payee tool — it never masks a
533
788
  * skew of a tool openclaw's core function depends on (those stay strict via `requireTool`).
789
+ *
790
+ * The group verbs are deliberately NOT lenient. `publish-package.sh` rewrites the `file:`
791
+ * mcp dep to a `^`-range on mcp's current version, so a published openclaw can never resolve
792
+ * an mcp without them; lenience would only take effect against a locally-linked older mcp,
793
+ * where it would let the plugin load with the group surface silently missing and every gate
794
+ * still green. That fake-green is the failure this set exists to avoid, not to create.
534
795
  */
535
796
  const LENIENT_TOOLS = new Set(["rine_fulfill"]);
536
797
  const CIPHERTEXT_KEYS = ["encrypted_payload", "self_encrypted_payload"];
@@ -584,7 +845,7 @@ function assertInternalTools(all = tools) {
584
845
  /**
585
846
  * Adapt one mcp ToolDef into an OpenClaw AnyAgentTool with ciphertext stripping.
586
847
  * `parameters` is rine-mcp's JSON-schema `inputSchema`; `registerTool` stores it
587
- * verbatim (no TypeBox validation see SDK_CONTRACT.md), so the whole tool object
848
+ * verbatim and validates none of it against TypeBox, so the whole tool object
588
849
  * is cast to `AnyAgentTool` at one documented seam. (typebox is a transitive dep of
589
850
  * openclaw, not directly importable, so we never reference its `TSchema` type.)
590
851
  *
@@ -630,15 +891,20 @@ function withAutoPayDefault(def, autoPay) {
630
891
  };
631
892
  }
632
893
  /**
633
- * Register the exposed rine tools on the plugin api. `rine_send`/`rine_onboard` and the
634
- * fund-moving `rine_pay`/`rine_fulfill` are registered `{ optional: true }` (allowlist
635
- * gating per manifest toolMetadata). Internal deps (`rine_reply`) are validated here too so
636
- * version skew fails at load. With `opts.autoPay`, `rine_pay` is threshold-gated by default.
894
+ * Register the exposed rine tools on the plugin api. The {@link OPTIONAL_TOOLS} register
895
+ * `{ optional: true }`, so OpenClaw offers them only when the agent's tool allowlist names
896
+ * them; every other tool `rine_onboard` included is offered under the default empty
897
+ * allowlist. Internal deps (`rine_reply`) are validated here too so version skew fails at
898
+ * load. With `opts.autoPay`, `rine_pay` is threshold-gated by default; with `opts.agentId`,
899
+ * every tool that declares an acting-agent argument defaults it to that agent (see
900
+ * {@link withActingAgent}). What each tool then does with the argument is rine-mcp's:
901
+ * nearly all of them send it as `X-Rine-Agent`, while `rine_read` reads it only to pick
902
+ * the identity it decrypts as and sends no header of its own.
637
903
  */
638
904
  function registerRineTools(api, ctx, opts = {}) {
639
905
  assertInternalTools(opts.tools);
640
906
  for (const def of selectExposedTools(opts.tools)) {
641
- const tuned = withAutoPayDefault(def, opts.autoPay === true);
907
+ const tuned = withActingAgent(withAutoPayDefault(def, opts.autoPay === true), opts.agentId);
642
908
  const regOpts = OPTIONAL_TOOLS.has(def.name) ? {
643
909
  name: def.name,
644
910
  optional: true
@@ -665,7 +931,10 @@ var rine_openclaw_default = defineChannelPluginEntry({
665
931
  },
666
932
  registerFull(api) {
667
933
  const config = resolveRineConfigFromCfg(api.config);
668
- registerRineTools(api, buildRineClient(readRineCredentials(config)).toolContext, { autoPay: config.payments.autoPay });
934
+ registerRineTools(api, buildRineClient(readRineCredentials(config)).toolContext, {
935
+ autoPay: config.payments.autoPay,
936
+ agentId: config.agentId
937
+ });
669
938
  registerExposeRoute(api);
670
939
  api.registerService(makeRineService(api));
671
940
  }
@@ -1,5 +1,4 @@
1
- import { r as normalizeRineEvent } from "./inbound-DEe69mNL.js";
2
- import { n as sleep } from "./backoff-BMNABavv.js";
1
+ import { a as normalizeRineEvent, n as sleep } from "./backoff-a3Dly8X4.js";
3
2
  //#region src/transports/poll.ts
4
3
  /**
5
4
  * POLL transport — least token-intensive. Fixed-interval `GET /poll/{token}` (unauth);
package/dist/setup.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resolveRineConfig, i as readRineCredentials, t as rinePlugin } from "./channel-DqxESY90.js";
1
+ import { a as readRineCredentials, o as resolveRineConfig, t as rinePlugin } from "./channel-DiPLuJsk.js";
2
2
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
3
3
  //#region setup.ts
4
4
  /** Auto-detect existing rine creds for the setup flow. Pure read; no writes. */
@@ -9,6 +9,8 @@ export interface RineAccount {
9
9
  * the notify service + dispatch seam (the canonical reply path lives on the runtime
10
10
  * singleton, available to the service), so the channel object stays thin — it advertises
11
11
  * the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
12
- * surfaces in `plugins inspect`. See SDK_CONTRACT.md.
12
+ * surfaces in `plugins inspect`. Those four are what `ChannelPlugin` itself requires;
13
+ * everything else on this object is setup, reload and target resolution, plus the
14
+ * outbound adapter core needs before it will deliver to a rine handle at all.
13
15
  */
14
16
  export declare const rinePlugin: ChannelPlugin<RineAccount>;
@@ -13,9 +13,12 @@ export declare function sendRineReply(client: RineClient, inbound: RineInbound,
13
13
  * and delivers (without this the channel had no outbound surface — the agent's
14
14
  * generic send failed with `Unknown target … for rine`).
15
15
  *
16
- * `from` (the sending agent id) is passed only when configured; in a single-agent
17
- * org `rine_send` resolves the sole agent itself.
16
+ * `agent` (the sending agent, by name, handle or UUID) is passed only when configured;
17
+ * without it `rine_send` climbs its own ladder — `RINE_AGENT`, then the org's sole
18
+ * agent — and refuses in a multi-agent org rather than guessing. It was spelled `from`
19
+ * here until the rename; `rine_send` still reads that word from a stale caller, but
20
+ * nothing in this plugin writes it.
18
21
  */
19
- export declare function sendRineText(client: RineClient, to: string, text: string, from?: string): Promise<{
22
+ export declare function sendRineText(client: RineClient, to: string, text: string, agent?: string): Promise<{
20
23
  messageId: string;
21
24
  }>;
@@ -39,5 +39,10 @@ export interface RineClient {
39
39
  * Build the rine HTTP client + ToolContext from resolved creds. Mirrors
40
40
  * rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
41
41
  * new HttpClient({ tokenFn, apiUrl, canRefresh }).
42
+ *
43
+ * The credential entry is resolved on every token mint rather than captured once, so a
44
+ * client built before the install had credentials keeps working after `rine_onboard`
45
+ * writes them — without it the disk-cached onboarding token is the only thing the client
46
+ * ever has, and every call fails once that token reaches its refresh margin.
42
47
  */
43
48
  export declare function buildRineClient(creds: ResolvedCreds): RineClient;