@rine-network/openclaw 0.3.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/README.md +79 -11
- package/dist/{inbound-DEe69mNL.js → backoff-a3Dly8X4.js} +33 -1
- package/dist/{channel-gwGzeOb6.js → channel-DiPLuJsk.js} +21 -11
- package/dist/index.js +346 -77
- package/dist/{poll-bXndKyw3.js → poll-BWvVevTN.js} +2 -3
- package/dist/setup.d.ts +2 -2
- package/dist/setup.js +1 -1
- package/dist/src/channel.d.ts +3 -1
- package/dist/src/dispatch.d.ts +3 -3
- package/dist/src/outbound.d.ts +6 -3
- package/dist/src/rine-client.d.ts +7 -3
- package/dist/src/runtime-dispatch.d.ts +16 -5
- package/dist/src/service.d.ts +2 -12
- package/dist/src/tool-actor.d.ts +52 -0
- package/dist/src/tools.d.ts +51 -8
- package/dist/src/transports/poll.d.ts +1 -1
- package/dist/src/types.d.ts +11 -3
- package/dist/{sse-BEULwHS1.js → sse-jRmQV4GZ.js} +2 -3
- package/openclaw.plugin.json +27 -7
- package/package.json +4 -5
- package/skills/rine/SKILL.md +232 -273
- package/skills/rine/references/openclaw.md +99 -14
- package/dist/backoff-BMNABavv.js +0 -33
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as
|
|
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 {
|
|
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
|
|
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": "
|
|
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",
|
|
@@ -102,7 +102,7 @@ function msgOf(err) {
|
|
|
102
102
|
/**
|
|
103
103
|
* Build the shared `onMessage` path all transports converge on.
|
|
104
104
|
*
|
|
105
|
-
* Invariants
|
|
105
|
+
* Invariants:
|
|
106
106
|
* - dedupe by rine message id (Set, add-id-BEFORE-await race guard; delete-on-error
|
|
107
107
|
* so a failed dispatch is retried; hourly clear bounds memory).
|
|
108
108
|
* - mark-delivered ONLY after a successful dispatch (at-least-once).
|
|
@@ -150,44 +150,84 @@ function makeOnMessage(deps) {
|
|
|
150
150
|
//#endregion
|
|
151
151
|
//#region src/runtime-dispatch.ts
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* the rine-hermes `_FUNNEL_WEBHOOK_TYPE` precedent.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
-
|
|
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 (
|
|
231
|
-
logger.debug?.(`rine: no auto-reply for
|
|
272
|
+
if (suppressed) {
|
|
273
|
+
logger.debug?.(`rine: no auto-reply for ${msg.type} ${msg.id} (${suppressed})`);
|
|
232
274
|
return;
|
|
233
275
|
}
|
|
234
|
-
if (
|
|
235
|
-
logger.
|
|
276
|
+
if (!route) {
|
|
277
|
+
logger.error(`rine: cannot answer group message ${msg.id} — inbound carries no group handle`);
|
|
236
278
|
return;
|
|
237
279
|
}
|
|
238
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
462
|
+
const { runSseTransport } = await import("./sse-jRmQV4GZ.js");
|
|
415
463
|
return runSseTransport(tc);
|
|
416
464
|
}
|
|
417
465
|
}
|
|
418
466
|
}
|
|
419
467
|
/**
|
|
420
|
-
* Resolve the
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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
|
|
433
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
"
|
|
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
|
|
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
|
*
|
|
@@ -611,7 +872,7 @@ function toOpenClawTool(def, ctx) {
|
|
|
611
872
|
};
|
|
612
873
|
}
|
|
613
874
|
/**
|
|
614
|
-
* Opt-in auto-pay
|
|
875
|
+
* Opt-in auto-pay. When `channels.rine.payments.autoPay` is true, the exposed
|
|
615
876
|
* `rine_pay` tool defaults its `auto_pay` argument to `true` — so a call that omits the
|
|
616
877
|
* argument pays only at/below the wallet policy's `autoPayThreshold` (and returns
|
|
617
878
|
* `above-auto-pay-threshold` otherwise). The model may still pass `auto_pay: false`
|
|
@@ -630,15 +891,20 @@ function withAutoPayDefault(def, autoPay) {
|
|
|
630
891
|
};
|
|
631
892
|
}
|
|
632
893
|
/**
|
|
633
|
-
* Register the exposed rine tools on the plugin api.
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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, {
|
|
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,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
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);
|
|
6
5
|
* only on `count > 0` does it mint a JWT, fetch `/messages?status=new`, and dispatch.
|
|
7
6
|
*
|
|
8
7
|
* Graceful fallback: the loop never crashes. A revoked/expired token (`401`/`403`) is
|
|
9
|
-
* surfaced as an actionable error exactly once (
|
|
8
|
+
* surfaced as an actionable error exactly once (not masked as an empty inbox);
|
|
10
9
|
* a transient network/HTTP error warns once; a healthy count of 0 is silent no-work. Every
|
|
11
10
|
* failure keeps the loop alive at the fixed interval, and a healthy poll re-arms the warnings.
|
|
12
11
|
*/
|
package/dist/setup.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup-safe entry for the rine channel. Loaded in setup/discovery registration modes.
|
|
3
|
-
* Side-effect-free at module top level. The transport posture is a config enum
|
|
4
|
-
*
|
|
3
|
+
* Side-effect-free at module top level. The transport posture is a config enum surfaced via
|
|
4
|
+
* the manifest configSchema + uiHints — OpenClaw has no native
|
|
5
5
|
* radio-select setup primitive, so the human sets `channels.rine.transport` directly.
|
|
6
6
|
*
|
|
7
7
|
* Credential auto-detection (no re-auth for an already-onboarded agent): the wizard
|
package/dist/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
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. */
|