@rine-network/openclaw 0.1.5 → 0.2.1
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 +27 -5
- package/dist/{channel-DpHhVQ-n.js → channel-BPBnthxw.js} +41 -13
- package/dist/{inbound-G0JD7YmI.js → inbound-DEe69mNL.js} +3 -0
- package/dist/index.js +34 -11
- package/dist/poll-bXndKyw3.js +61 -0
- package/dist/setup.js +1 -1
- package/dist/src/config.d.ts +12 -1
- package/dist/src/constants.d.ts +6 -0
- package/dist/src/dispatch.d.ts +14 -2
- package/dist/src/rine-client.d.ts +21 -2
- package/dist/src/tools.d.ts +1 -1
- package/dist/src/transports/poll.d.ts +4 -4
- package/dist/src/types.d.ts +7 -0
- package/dist/{sse-CLGVjmVC.js → sse-BEULwHS1.js} +11 -5
- package/openclaw.plugin.json +2 -1
- package/package.json +3 -3
- package/skills/rine/SKILL.md +23 -2
- package/dist/poll-BvmG87ve.js +0 -42
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ conversation). The agent can also actively send/read/discover via tools.
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# 1. Install and enable the plugin
|
|
15
|
-
openclaw plugins install
|
|
15
|
+
openclaw plugins install @rine-network/openclaw
|
|
16
16
|
openclaw plugins enable rine
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -42,8 +42,8 @@ openclaw gateway restart
|
|
|
42
42
|
openclaw plugins inspect rine --runtime --json # verify channel + tools + service + route
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Published on npm only
|
|
46
|
-
`openclaw plugins install
|
|
45
|
+
Published on npm only; the explicit spec
|
|
46
|
+
`openclaw plugins install npm:@rine-network/openclaw` works too. The Gateway warns when
|
|
47
47
|
`plugins.allow` is empty — for a locked-down host, add `rine` to `plugins.allow` in
|
|
48
48
|
`openclaw.json` so only trusted plugin ids auto-load.
|
|
49
49
|
|
|
@@ -122,10 +122,26 @@ Optional A2A per-task push (`CreateTaskPushNotificationConfig`) is a layer on to
|
|
|
122
122
|
standard webhook (it needs an existing conversation/taskId); the `/rine/inbound` handler
|
|
123
123
|
normalizes both standard-webhook and A2A `artifactUpdate` envelopes.
|
|
124
124
|
|
|
125
|
+
## Inbound webhooks (the rine Funnel)
|
|
126
|
+
|
|
127
|
+
An OpenClaw agent can receive external webhook sources (GitHub, Stripe, or any
|
|
128
|
+
HMAC-signing sender) as encrypted inbox messages through the
|
|
129
|
+
[rine Funnel](https://rine.network/funnel.md). Run `rine hook create` to allocate a public
|
|
130
|
+
hostname and secret, then a long-lived `rine relay` on the Gateway box to verify each
|
|
131
|
+
request, encrypt it to the agent's key, and self-send it as a `rine.v1.webhook` message
|
|
132
|
+
(the originating hook name is in cleartext metadata at `rine.hook_name`).
|
|
133
|
+
|
|
134
|
+
A Funnel webhook is a **one-way event**: the relay sends it on the agent's behalf, so its
|
|
135
|
+
sender is the agent's own handle. Act on the webhook — don't reply to its sender. A reply
|
|
136
|
+
targets the agent itself, which rine rejects (OpenClaw logs a non-fatal
|
|
137
|
+
`Cannot reply to your own message` and carries on); to follow up, send a new message to a
|
|
138
|
+
real peer. With a non-`*` `channels.rine.allowFrom`, add the agent's own handle (or its
|
|
139
|
+
`@org`) or the self-sent webhook is quarantined.
|
|
140
|
+
|
|
125
141
|
## Tools
|
|
126
142
|
|
|
127
|
-
`rine_whoami`, `rine_discover`, `rine_read`, `rine_inbox`, and
|
|
128
|
-
`rine_send`, `rine_onboard`. Decryption happens on demand inside the handler; the raw
|
|
143
|
+
`rine_whoami`, `rine_discover`, `rine_read`, `rine_inbox`, `rine_thread`, and
|
|
144
|
+
(allowlist-gated, mutating) `rine_send`, `rine_onboard`. Decryption happens on demand inside the handler; the raw
|
|
129
145
|
`encrypted_payload` is **never** surfaced to a transcript — only `decrypted` + `verified`.
|
|
130
146
|
|
|
131
147
|
`rine_send` / `rine_onboard` are `optional` tools — allowlist them (or run with an approval
|
|
@@ -162,6 +178,12 @@ openclaw plugins doctor
|
|
|
162
178
|
- **`npm ... ENOENT ... mkdir '…/.npm'` while installing:** read-only-rootfs host — npm can't
|
|
163
179
|
write its default cache. See _Hardened / read-only-rootfs containers_ under **Install**.
|
|
164
180
|
|
|
181
|
+
## For AI Agents
|
|
182
|
+
|
|
183
|
+
- [Platform docs](https://rine.network/llms.txt)
|
|
184
|
+
- [OpenClaw integration docs](https://rine.network/openclaw.md)
|
|
185
|
+
- [Protocol](https://rine.network/protocol.md)
|
|
186
|
+
|
|
165
187
|
## License
|
|
166
188
|
|
|
167
189
|
EUPL-1.2.
|
|
@@ -12,6 +12,12 @@ import { tools } from "@rine-network/mcp/tools";
|
|
|
12
12
|
*/
|
|
13
13
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
14
14
|
/**
|
|
15
|
+
* The rine channel / plugin id. It is the key under both `channels.<id>` (activation +
|
|
16
|
+
* effective config) and `plugins.entries.<id>` in openclaw.json, and the `id` this plugin
|
|
17
|
+
* registers under. Shared so the config resolver reads the same block the channel activates.
|
|
18
|
+
*/
|
|
19
|
+
const CHANNEL_ID = "rine";
|
|
20
|
+
/**
|
|
15
21
|
* Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
|
|
16
22
|
* Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
|
|
17
23
|
* fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path;
|
|
@@ -43,7 +49,7 @@ function asAllowFrom(v) {
|
|
|
43
49
|
}
|
|
44
50
|
return ["*"];
|
|
45
51
|
}
|
|
46
|
-
/** Resolve the typed config from a raw
|
|
52
|
+
/** Resolve the typed config from a raw config record (or `{}`), applying defaults. */
|
|
47
53
|
function resolveRineConfig(raw = {}) {
|
|
48
54
|
return {
|
|
49
55
|
transport: asTransport(raw.transport),
|
|
@@ -58,6 +64,28 @@ function resolveRineConfig(raw = {}) {
|
|
|
58
64
|
allowFrom: asAllowFrom(raw.allowFrom)
|
|
59
65
|
};
|
|
60
66
|
}
|
|
67
|
+
function asRecord(v) {
|
|
68
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the typed config from a live OpenClawConfig (`api.config` / an outbound `ctx.cfg`).
|
|
72
|
+
*
|
|
73
|
+
* `channels.rine` is the documented, activation-required home for `transport`/`allowFrom`/etc.,
|
|
74
|
+
* so it is authoritative; `plugins.entries.rine.config` is merged underneath as a fallback for
|
|
75
|
+
* installs that placed config there (both are validated against the same manifest schema). This
|
|
76
|
+
* is the single source of truth for the notify service, the tool client, and the outbound
|
|
77
|
+
* adapter — before, the service/tools read `api.pluginConfig` (`plugins.entries.rine.config`)
|
|
78
|
+
* while outbound read `channels.rine`, so `channels.rine.transport` was silently ignored.
|
|
79
|
+
*/
|
|
80
|
+
function resolveRineConfigFromCfg(cfg) {
|
|
81
|
+
const root = asRecord(cfg);
|
|
82
|
+
const channelCfg = asRecord(asRecord(root.channels)[CHANNEL_ID]);
|
|
83
|
+
const entryCfg = asRecord(asRecord(asRecord(root.plugins).entries)[CHANNEL_ID]).config;
|
|
84
|
+
return resolveRineConfig({
|
|
85
|
+
...asRecord(entryCfg),
|
|
86
|
+
...channelCfg
|
|
87
|
+
});
|
|
88
|
+
}
|
|
61
89
|
/**
|
|
62
90
|
* Resolve rine credentials using core's 3-level config-dir fallback
|
|
63
91
|
* ($RINE_CONFIG_DIR > ~/.config/rine > $PWD/.rine). An explicit `cfg.configDir`
|
|
@@ -136,11 +164,18 @@ function buildRineClient(creds) {
|
|
|
136
164
|
async function pollCount(pollUrl, signal) {
|
|
137
165
|
try {
|
|
138
166
|
const res = await fetch(pollUrl, { signal: signal ?? AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS) });
|
|
139
|
-
if (
|
|
167
|
+
if (res.status === 401 || res.status === 403) return {
|
|
168
|
+
kind: "auth-error",
|
|
169
|
+
status: res.status
|
|
170
|
+
};
|
|
171
|
+
if (!res.ok) return { kind: "error" };
|
|
140
172
|
const body = await res.json();
|
|
141
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
kind: "count",
|
|
175
|
+
count: typeof body.count === "number" ? body.count : 0
|
|
176
|
+
};
|
|
142
177
|
} catch {
|
|
143
|
-
return
|
|
178
|
+
return { kind: "error" };
|
|
144
179
|
}
|
|
145
180
|
}
|
|
146
181
|
async function fetchNewMessages(agentId, limit = 50) {
|
|
@@ -165,13 +200,6 @@ function buildRineClient(creds) {
|
|
|
165
200
|
markDelivered
|
|
166
201
|
};
|
|
167
202
|
}
|
|
168
|
-
//#endregion
|
|
169
|
-
//#region src/channel.ts
|
|
170
|
-
/** Re-derive the typed rine config from the `channels.rine` block of a live OpenClawConfig. */
|
|
171
|
-
function rineConfigFromCfg(cfg) {
|
|
172
|
-
const channels = cfg.channels;
|
|
173
|
-
return resolveRineConfig(channels?.rine ?? {});
|
|
174
|
-
}
|
|
175
203
|
/**
|
|
176
204
|
* Minimal but type-valid `ChannelPlugin` for rine. Required fields only:
|
|
177
205
|
* id / meta / capabilities / config. Inbound delivery + outbound replies are owned by
|
|
@@ -252,7 +280,7 @@ const rinePlugin = {
|
|
|
252
280
|
};
|
|
253
281
|
},
|
|
254
282
|
sendText: async (ctx) => {
|
|
255
|
-
const config =
|
|
283
|
+
const config = resolveRineConfigFromCfg(ctx.cfg);
|
|
256
284
|
const creds = readRineCredentials(config);
|
|
257
285
|
if (!creds.entry) throw new Error("rine: no credentials at the resolved config dir — run rine_onboard or set RINE_CONFIG_DIR.");
|
|
258
286
|
const { messageId } = await sendRineText(buildRineClient(creds), ctx.to, ctx.text, config.agentId);
|
|
@@ -274,4 +302,4 @@ const rinePlugin = {
|
|
|
274
302
|
} }
|
|
275
303
|
};
|
|
276
304
|
//#endregion
|
|
277
|
-
export { resolveRineConfig as a, readRineCredentials as i, buildRineClient as n,
|
|
305
|
+
export { resolveRineConfig as a, readRineCredentials as i, buildRineClient as n, resolveRineConfigFromCfg as o, sendRineReply as r, INTERNAL_TOOLS as s, rinePlugin as t };
|
|
@@ -10,11 +10,14 @@ function normalizeRineEvent(raw) {
|
|
|
10
10
|
const groupHandle = str(raw.group_handle);
|
|
11
11
|
const groupId = str(raw.group_id);
|
|
12
12
|
const isGroup = Boolean(groupHandle ?? groupId);
|
|
13
|
+
const fromAgentId = str(raw.from_agent_id);
|
|
14
|
+
const toAgentId = str(raw.to_agent_id);
|
|
13
15
|
return {
|
|
14
16
|
id: raw.id,
|
|
15
17
|
conversationId: raw.conversation_id,
|
|
16
18
|
fromHandle: str(raw.sender_handle) ?? str(raw.from_agent_id) ?? "unknown",
|
|
17
19
|
type: raw.type,
|
|
20
|
+
selfSend: Boolean(fromAgentId && fromAgentId === toAgentId),
|
|
18
21
|
isGroup,
|
|
19
22
|
groupHandle,
|
|
20
23
|
groupId,
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as normalizeStandardWebhook, n as normalizeA2A, t as isAllowed } from "./inbound-
|
|
1
|
+
import { i as readRineCredentials, n as buildRineClient, o as resolveRineConfigFromCfg, r as sendRineReply, s as INTERNAL_TOOLS, t as rinePlugin } from "./channel-BPBnthxw.js";
|
|
2
|
+
import { i as normalizeStandardWebhook, n as normalizeA2A, t as isAllowed } from "./inbound-DEe69mNL.js";
|
|
3
3
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
4
4
|
import { fetchAgents, resolveAgent } from "@rine-network/core";
|
|
5
5
|
import { tools } from "@rine-network/mcp/tools";
|
|
@@ -85,6 +85,20 @@ var configSchema = {
|
|
|
85
85
|
};
|
|
86
86
|
//#endregion
|
|
87
87
|
//#region src/dispatch.ts
|
|
88
|
+
/**
|
|
89
|
+
* The Funnel relay self-sends this type (`from == to`); it is a one-way event the agent ACTS
|
|
90
|
+
* on and must never auto-reply to (the reply is a self-send the server rejects). Compared as a
|
|
91
|
+
* literal so the guard never depends on the installed SDK exporting a named constant — mirrors
|
|
92
|
+
* the rine-hermes `_FUNNEL_WEBHOOK_TYPE` precedent.
|
|
93
|
+
*/
|
|
94
|
+
const WEBHOOK_TYPE = "rine.v1.webhook";
|
|
95
|
+
/**
|
|
96
|
+
* A one-way inbound: a Funnel webhook, or any self-addressed message (`from == to`). The agent
|
|
97
|
+
* wakes to act via tools, but an auto-reply would target itself and be rejected (REQ-FSH-02).
|
|
98
|
+
*/
|
|
99
|
+
function isOneWayEvent(msg) {
|
|
100
|
+
return msg.type === WEBHOOK_TYPE || msg.selfSend === true;
|
|
101
|
+
}
|
|
88
102
|
function msgOf(err) {
|
|
89
103
|
return err instanceof Error ? err.message : String(err);
|
|
90
104
|
}
|
|
@@ -120,7 +134,7 @@ function makeOnMessage(deps) {
|
|
|
120
134
|
lastClear = Date.now();
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
|
-
return async function onMessage(msg) {
|
|
137
|
+
return async function onMessage(msg, opts) {
|
|
124
138
|
maybeClear();
|
|
125
139
|
if (seen.has(msg.id)) return true;
|
|
126
140
|
if (isAllowed(msg.fromHandle, config.allowFrom) === "quarantined") {
|
|
@@ -133,7 +147,7 @@ function makeOnMessage(deps) {
|
|
|
133
147
|
logger.info(`rine: inbound ${msg.type} ${msg.id} from ${msg.fromHandle} → dispatching`);
|
|
134
148
|
seen.add(msg.id);
|
|
135
149
|
try {
|
|
136
|
-
await dispatch(msg, signal);
|
|
150
|
+
await dispatch(msg, signal, opts);
|
|
137
151
|
} catch (err) {
|
|
138
152
|
seen.delete(msg.id);
|
|
139
153
|
logger.error(`rine: dispatch failed for ${msg.id}: ${msgOf(err)}`);
|
|
@@ -157,7 +171,7 @@ function makeOnMessage(deps) {
|
|
|
157
171
|
*/
|
|
158
172
|
function makeRuntimeDispatcher(params) {
|
|
159
173
|
const { cfg, runtime, client, agentId, logger } = params;
|
|
160
|
-
return async function dispatch(msg, signal) {
|
|
174
|
+
return async function dispatch(msg, signal, opts) {
|
|
161
175
|
signal.throwIfAborted();
|
|
162
176
|
const peerId = msg.isGroup ? msg.conversationId : msg.fromHandle;
|
|
163
177
|
const timestamp = msg.createdAt ? Date.parse(msg.createdAt) : void 0;
|
|
@@ -185,6 +199,14 @@ function makeRuntimeDispatcher(params) {
|
|
|
185
199
|
logger.warn(`rine: dropping non-text reply for ${msg.id} (rine replies are text-only)`);
|
|
186
200
|
return;
|
|
187
201
|
}
|
|
202
|
+
if (isOneWayEvent(msg)) {
|
|
203
|
+
logger.debug?.(`rine: no auto-reply for one-way ${msg.type} ${msg.id} (act via tools; reply would target self)`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (opts?.replayed) {
|
|
207
|
+
logger.debug?.(`rine: no auto-reply for catch-up/replayed ${msg.id} (live-phase frames only)`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
188
210
|
await replyToRine(client, msg, text, logger);
|
|
189
211
|
},
|
|
190
212
|
onRecordError: (err) => logger.warn(`rine: session-record error for ${msg.id}: ${msgOf(err)}`),
|
|
@@ -333,7 +355,7 @@ async function deleteWebhook(client, webhookId, logger) {
|
|
|
333
355
|
}
|
|
334
356
|
}
|
|
335
357
|
async function fallbackToSse(tc) {
|
|
336
|
-
const { runSseTransport } = await import("./sse-
|
|
358
|
+
const { runSseTransport } = await import("./sse-BEULwHS1.js");
|
|
337
359
|
await runSseTransport(tc);
|
|
338
360
|
}
|
|
339
361
|
function waitUntilAbort(signal) {
|
|
@@ -352,12 +374,12 @@ const SERVICE_ID = "rine-notify";
|
|
|
352
374
|
async function runTransport(tc) {
|
|
353
375
|
switch (tc.config.transport) {
|
|
354
376
|
case "poll": {
|
|
355
|
-
const { runPollTransport } = await import("./poll-
|
|
377
|
+
const { runPollTransport } = await import("./poll-bXndKyw3.js");
|
|
356
378
|
return runPollTransport(tc);
|
|
357
379
|
}
|
|
358
380
|
case "expose": return runExposeTransport(tc);
|
|
359
381
|
default: {
|
|
360
|
-
const { runSseTransport } = await import("./sse-
|
|
382
|
+
const { runSseTransport } = await import("./sse-BEULwHS1.js");
|
|
361
383
|
return runSseTransport(tc);
|
|
362
384
|
}
|
|
363
385
|
}
|
|
@@ -424,7 +446,7 @@ function makeRineService(api) {
|
|
|
424
446
|
id: SERVICE_ID,
|
|
425
447
|
start(ctx) {
|
|
426
448
|
const logger = ctx.logger ?? api.logger;
|
|
427
|
-
const config =
|
|
449
|
+
const config = resolveRineConfigFromCfg(api.config);
|
|
428
450
|
const creds = readRineCredentials(config);
|
|
429
451
|
if (!creds.entry) {
|
|
430
452
|
logger.warn(`rine: no credentials.json at ${creds.configDir} — notify service idle (run rine_onboard or set RINE_CONFIG_DIR)`);
|
|
@@ -459,11 +481,12 @@ const EXPOSED_TOOLS = [
|
|
|
459
481
|
"rine_send",
|
|
460
482
|
"rine_read",
|
|
461
483
|
"rine_inbox",
|
|
484
|
+
"rine_thread",
|
|
462
485
|
"rine_onboard"
|
|
463
486
|
];
|
|
464
487
|
/** Mutating tools gated behind the allowlist (manifest `toolMetadata.optional`). */
|
|
465
488
|
const OPTIONAL_TOOLS = new Set(["rine_send", "rine_onboard"]);
|
|
466
|
-
const CIPHERTEXT_KEYS = ["encrypted_payload"];
|
|
489
|
+
const CIPHERTEXT_KEYS = ["encrypted_payload", "self_encrypted_payload"];
|
|
467
490
|
/**
|
|
468
491
|
* Strip raw ciphertext from any tool result before it reaches a transcript.
|
|
469
492
|
* Recurses through objects/arrays so `rine_read`/`rine_inbox` (which spread the
|
|
@@ -564,7 +587,7 @@ var rine_openclaw_default = defineChannelPluginEntry({
|
|
|
564
587
|
uiHints
|
|
565
588
|
},
|
|
566
589
|
registerFull(api) {
|
|
567
|
-
registerRineTools(api, buildRineClient(readRineCredentials(
|
|
590
|
+
registerRineTools(api, buildRineClient(readRineCredentials(resolveRineConfigFromCfg(api.config))).toolContext);
|
|
568
591
|
registerExposeRoute(api);
|
|
569
592
|
api.registerService(makeRineService(api));
|
|
570
593
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { r as normalizeRineEvent } from "./inbound-DEe69mNL.js";
|
|
2
|
+
import { n as sleep } from "./backoff-BMNABavv.js";
|
|
3
|
+
//#region src/transports/poll.ts
|
|
4
|
+
/**
|
|
5
|
+
* POLL transport — least token-intensive. Fixed-interval `GET /poll/{token}` (unauth);
|
|
6
|
+
* only on `count > 0` does it mint a JWT, fetch `/messages?status=new`, and dispatch.
|
|
7
|
+
*
|
|
8
|
+
* Graceful fallback: the loop never crashes. A revoked/expired token (`401`/`403`) is
|
|
9
|
+
* surfaced as an actionable error exactly once (REQ-FSH-06 — not masked as an empty inbox);
|
|
10
|
+
* a transient network/HTTP error warns once; a healthy count of 0 is silent no-work. Every
|
|
11
|
+
* failure keeps the loop alive at the fixed interval, and a healthy poll re-arms the warnings.
|
|
12
|
+
*/
|
|
13
|
+
async function runPollTransport(tc) {
|
|
14
|
+
const { client, config, agentId, logger, onMessage, signal, pollUrl } = tc;
|
|
15
|
+
if (!pollUrl) {
|
|
16
|
+
logger.error("rine: POLL transport requires a poll_url in credentials.json (.default.poll_url) — run `rine poll-token`");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let warnedKind;
|
|
20
|
+
while (!signal.aborted) {
|
|
21
|
+
const result = await client.pollCount(pollUrl, signal);
|
|
22
|
+
if (result.kind === "auth-error") {
|
|
23
|
+
if (warnedKind !== "auth") {
|
|
24
|
+
warnedKind = "auth";
|
|
25
|
+
logger.error(`rine: POLL token rejected (HTTP ${result.status}) — inbox is silently dead until you run \`rine poll-token\` and update credentials.json (.default.poll_url)`);
|
|
26
|
+
}
|
|
27
|
+
await wait(config.pollIntervalMs, signal);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (result.kind === "error") {
|
|
31
|
+
if (warnedKind !== "network") {
|
|
32
|
+
warnedKind = "network";
|
|
33
|
+
logger.warn("rine: POLL count request failed (network/transient) — keeping loop alive");
|
|
34
|
+
}
|
|
35
|
+
await wait(config.pollIntervalMs, signal);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
warnedKind = void 0;
|
|
39
|
+
if (result.count <= 0) {
|
|
40
|
+
await wait(config.pollIntervalMs, signal);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const items = await client.fetchNewMessages(agentId);
|
|
45
|
+
for (const raw of items) {
|
|
46
|
+
if (signal.aborted) break;
|
|
47
|
+
await onMessage(normalizeRineEvent(raw));
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
logger.warn(`rine: POLL fetch/dispatch error (keeping loop alive): ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
}
|
|
52
|
+
await wait(config.pollIntervalMs, signal);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function wait(ms, signal) {
|
|
56
|
+
try {
|
|
57
|
+
await sleep(ms, signal);
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { runPollTransport };
|
package/dist/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as resolveRineConfig, i as readRineCredentials, t as rinePlugin } from "./channel-
|
|
1
|
+
import { a as resolveRineConfig, i as readRineCredentials, t as rinePlugin } from "./channel-BPBnthxw.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. */
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { CredentialEntry } from "@rine-network/core";
|
|
2
2
|
import type { RineConfig } from "./types.js";
|
|
3
|
-
/** Resolve the typed config from a raw
|
|
3
|
+
/** Resolve the typed config from a raw config record (or `{}`), applying defaults. */
|
|
4
4
|
export declare function resolveRineConfig(raw?: Record<string, unknown>): RineConfig;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the typed config from a live OpenClawConfig (`api.config` / an outbound `ctx.cfg`).
|
|
7
|
+
*
|
|
8
|
+
* `channels.rine` is the documented, activation-required home for `transport`/`allowFrom`/etc.,
|
|
9
|
+
* so it is authoritative; `plugins.entries.rine.config` is merged underneath as a fallback for
|
|
10
|
+
* installs that placed config there (both are validated against the same manifest schema). This
|
|
11
|
+
* is the single source of truth for the notify service, the tool client, and the outbound
|
|
12
|
+
* adapter — before, the service/tools read `api.pluginConfig` (`plugins.entries.rine.config`)
|
|
13
|
+
* while outbound read `channels.rine`, so `channels.rine.transport` was silently ignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveRineConfigFromCfg(cfg: unknown): RineConfig;
|
|
5
16
|
export interface ResolvedCreds {
|
|
6
17
|
configDir: string;
|
|
7
18
|
apiUrl: string;
|
package/dist/src/constants.d.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* channel. Both are `"default"` by convention.
|
|
8
8
|
*/
|
|
9
9
|
export declare const DEFAULT_ACCOUNT_ID = "default";
|
|
10
|
+
/**
|
|
11
|
+
* The rine channel / plugin id. It is the key under both `channels.<id>` (activation +
|
|
12
|
+
* effective config) and `plugins.entries.<id>` in openclaw.json, and the `id` this plugin
|
|
13
|
+
* registers under. Shared so the config resolver reads the same block the channel activates.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CHANNEL_ID = "rine";
|
|
10
16
|
/**
|
|
11
17
|
* Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
|
|
12
18
|
* Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
|
package/dist/src/dispatch.d.ts
CHANGED
|
@@ -7,7 +7,19 @@ import type { RineConfig, RineInbound } from "./types.js";
|
|
|
7
7
|
* (`openclaw/plugin-sdk/channel-inbound`), the canonical inbound→agent-turn helper that
|
|
8
8
|
* routes, finalizes the PascalCase ctx, records the session, and dispatches.
|
|
9
9
|
*/
|
|
10
|
-
export type DispatchFn = (msg: RineInbound, signal: AbortSignal) => Promise<void>;
|
|
10
|
+
export type DispatchFn = (msg: RineInbound, signal: AbortSignal, opts?: DispatchOpts) => Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Per-message dispatch modifiers set by the transport (not part of the message itself).
|
|
13
|
+
*/
|
|
14
|
+
export interface DispatchOpts {
|
|
15
|
+
/**
|
|
16
|
+
* This frame was replayed during the SSE catch-up phase (before the stream reached the
|
|
17
|
+
* live/heartbeat state) — wake the agent to ACT, but never auto-reply. A restart against
|
|
18
|
+
* an undelivered backlog would otherwise fan out one canned reply per replayed message
|
|
19
|
+
* (REQ-FSH-01). Live-phase frames (opts absent / `replayed:false`) still auto-reply.
|
|
20
|
+
*/
|
|
21
|
+
replayed?: boolean;
|
|
22
|
+
}
|
|
11
23
|
export interface OnMessageDeps {
|
|
12
24
|
client: RineClient;
|
|
13
25
|
config: RineConfig;
|
|
@@ -23,7 +35,7 @@ export interface OnMessageDeps {
|
|
|
23
35
|
* or quarantined) so the SSE cursor may advance past it; `false` only when dispatch FAILED —
|
|
24
36
|
* a transient error that must be retried, so the cursor must stay behind it.
|
|
25
37
|
*/
|
|
26
|
-
export type OnMessage = (msg: RineInbound) => Promise<boolean>;
|
|
38
|
+
export type OnMessage = (msg: RineInbound, opts?: DispatchOpts) => Promise<boolean>;
|
|
27
39
|
/**
|
|
28
40
|
* The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
|
|
29
41
|
*
|
|
@@ -2,6 +2,21 @@ import { HttpClient } from "@rine-network/core";
|
|
|
2
2
|
import type { MessageRead } from "@rine-network/core";
|
|
3
3
|
import type { ToolContext } from "@rine-network/mcp/tools";
|
|
4
4
|
import type { ResolvedCreds } from "./config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Outcome of a `/poll` count probe. A revoked/expired poll token (`401`/`403`) is
|
|
7
|
+
* distinguished from a healthy count and from a transient network/HTTP error so the poll
|
|
8
|
+
* loop can surface an actionable auth warning instead of masking it as an empty inbox
|
|
9
|
+
* (REQ-FSH-06).
|
|
10
|
+
*/
|
|
11
|
+
export type PollResult = {
|
|
12
|
+
kind: "count";
|
|
13
|
+
count: number;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "auth-error";
|
|
16
|
+
status: number;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "error";
|
|
19
|
+
};
|
|
5
20
|
export interface RineClient {
|
|
6
21
|
/** Shared `ToolContext` for the lifted mcp tool handlers. */
|
|
7
22
|
toolContext: ToolContext;
|
|
@@ -10,8 +25,12 @@ export interface RineClient {
|
|
|
10
25
|
apiUrl: string;
|
|
11
26
|
/** Mint/refresh the OAuth JWT via core's token cache (auto-refresh on 401). */
|
|
12
27
|
getJwt: (force?: boolean) => Promise<string>;
|
|
13
|
-
/**
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Unauthenticated poll: `GET {pollUrl}` → a {@link PollResult}. Reports `auth-error` on a
|
|
30
|
+
* `401`/`403` (revoked token) and `error` on any transient network/HTTP failure, so the
|
|
31
|
+
* caller never confuses a dead token for an empty inbox (REQ-FSH-06).
|
|
32
|
+
*/
|
|
33
|
+
pollCount: (pollUrl: string, signal?: AbortSignal) => Promise<PollResult>;
|
|
15
34
|
/** Fetch new messages for an agent (auth). */
|
|
16
35
|
fetchNewMessages: (agentId: string, limit?: number) => Promise<MessageRead[]>;
|
|
17
36
|
/** Mark messages delivered (auth) — at-least-once after successful dispatch. */
|
package/dist/src/tools.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import type { AnyAgentTool } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { ToolContext, ToolDef } from "@rine-network/mcp/tools";
|
|
4
4
|
/** Tools exposed by the plugin (filtered from the mcp tool array). */
|
|
5
|
-
export declare const EXPOSED_TOOLS: readonly ["rine_whoami", "rine_discover", "rine_send", "rine_read", "rine_inbox", "rine_onboard"];
|
|
5
|
+
export declare const EXPOSED_TOOLS: readonly ["rine_whoami", "rine_discover", "rine_send", "rine_read", "rine_inbox", "rine_thread", "rine_onboard"];
|
|
6
6
|
/** Mutating tools gated behind the allowlist (manifest `toolMetadata.optional`). */
|
|
7
7
|
export declare const OPTIONAL_TOOLS: Set<string>;
|
|
8
8
|
/**
|
|
@@ -3,9 +3,9 @@ import type { TransportContext } from "./context.js";
|
|
|
3
3
|
* POLL transport — least token-intensive. Fixed-interval `GET /poll/{token}` (unauth);
|
|
4
4
|
* only on `count > 0` does it mint a JWT, fetch `/messages?status=new`, and dispatch.
|
|
5
5
|
*
|
|
6
|
-
* Graceful fallback:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Graceful fallback: the loop never crashes. A revoked/expired token (`401`/`403`) is
|
|
7
|
+
* surfaced as an actionable error exactly once (REQ-FSH-06 — not masked as an empty inbox);
|
|
8
|
+
* a transient network/HTTP error warns once; a healthy count of 0 is silent no-work. Every
|
|
9
|
+
* failure keeps the loop alive at the fixed interval, and a healthy poll re-arms the warnings.
|
|
10
10
|
*/
|
|
11
11
|
export declare function runPollTransport(tc: TransportContext): Promise<void>;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export interface RineInbound {
|
|
|
15
15
|
fromHandle: string;
|
|
16
16
|
/** rine message type, e.g. `rine.v1.dm`. */
|
|
17
17
|
type: string;
|
|
18
|
+
/**
|
|
19
|
+
* `from_agent_id == to_agent_id` — a self-addressed message. A Funnel `rine.v1.webhook`
|
|
20
|
+
* is the canonical case (the relay sends on the agent's behalf), and the server rejects a
|
|
21
|
+
* reply to it ("Cannot reply to your own message"). Drives the one-way no-auto-reply guard
|
|
22
|
+
* (REQ-FSH-02). Absent when the transport can't determine both agent ids (e.g. A2A push).
|
|
23
|
+
*/
|
|
24
|
+
selfSend?: boolean;
|
|
18
25
|
/** Whether this is a group/broadcast message. */
|
|
19
26
|
isGroup: boolean;
|
|
20
27
|
/** Group handle `#name@org` when `isGroup`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as normalizeRineEvent } from "./inbound-
|
|
1
|
+
import { r as normalizeRineEvent } from "./inbound-DEe69mNL.js";
|
|
2
2
|
import { n as sleep, t as backoff } from "./backoff-BMNABavv.js";
|
|
3
3
|
import { n as saveCursor, t as loadCursor } from "./cursor-DPl6-dKW.js";
|
|
4
4
|
//#region src/transports/sse.ts
|
|
@@ -49,7 +49,7 @@ async function runSseTransport(tc) {
|
|
|
49
49
|
onReset: () => {
|
|
50
50
|
attempt = 0;
|
|
51
51
|
},
|
|
52
|
-
onRaw: async (raw) => onMessage(normalizeRineEvent(raw))
|
|
52
|
+
onRaw: async (raw, opts) => onMessage(normalizeRineEvent(raw), opts)
|
|
53
53
|
});
|
|
54
54
|
attempt = 0;
|
|
55
55
|
} catch (err) {
|
|
@@ -57,7 +57,7 @@ async function runSseTransport(tc) {
|
|
|
57
57
|
logger.warn(`rine: SSE stream error (attempt ${attempt}): ${err instanceof Error ? err.message : String(err)}`);
|
|
58
58
|
if (attempt >= MAX_ATTEMPTS_BEFORE_FALLBACK) {
|
|
59
59
|
logger.warn("rine: SSE failed repeatedly — falling back to POLL transport");
|
|
60
|
-
const { runPollTransport } = await import("./poll-
|
|
60
|
+
const { runPollTransport } = await import("./poll-bXndKyw3.js");
|
|
61
61
|
await runPollTransport(tc);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -89,6 +89,7 @@ async function streamOnce(params) {
|
|
|
89
89
|
const decoder = new TextDecoder();
|
|
90
90
|
let buffer = "";
|
|
91
91
|
let gapSeen = false;
|
|
92
|
+
let live = lastEventId !== void 0;
|
|
92
93
|
while (!signal.aborted) {
|
|
93
94
|
const { done, value } = await readWithIdleWatchdog(reader);
|
|
94
95
|
if (done) return;
|
|
@@ -98,10 +99,15 @@ async function streamOnce(params) {
|
|
|
98
99
|
const block = buffer.slice(0, sep);
|
|
99
100
|
buffer = buffer.slice(sep + 2);
|
|
100
101
|
const evt = parseSseBlock(block);
|
|
101
|
-
if (!evt
|
|
102
|
+
if (!evt) continue;
|
|
103
|
+
if (evt.event === "heartbeat" || evt.event === "status") {
|
|
104
|
+
live = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (evt.event !== "message" || !evt.data) continue;
|
|
102
108
|
let delivered = false;
|
|
103
109
|
try {
|
|
104
|
-
delivered = await onRaw(JSON.parse(evt.data));
|
|
110
|
+
delivered = await onRaw(JSON.parse(evt.data), { replayed: !live });
|
|
105
111
|
} catch {
|
|
106
112
|
if (evt.id) gapSeen = true;
|
|
107
113
|
continue;
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"kind": "channel",
|
|
4
4
|
"name": "rine",
|
|
5
5
|
"description": "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
|
|
6
|
-
"version": "0.1
|
|
6
|
+
"version": "0.2.1",
|
|
7
7
|
"channels": ["rine"],
|
|
8
8
|
"skills": ["skills/rine"],
|
|
9
9
|
"activation": { "onStartup": true },
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"rine_send",
|
|
15
15
|
"rine_read",
|
|
16
16
|
"rine_inbox",
|
|
17
|
+
"rine_thread",
|
|
17
18
|
"rine_onboard"
|
|
18
19
|
]
|
|
19
20
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rine-network/openclaw",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Official OpenClaw plugin for rine.network \u2014 agent-to-agent E2EE messaging as a native channel, with A2A-relay / SSE / poll transports, tools, and the bundled rine skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"prepublishOnly": "node scripts/check-no-file-deps.mjs"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@rine-network/core": "^0.
|
|
68
|
-
"@rine-network/mcp": "^0.
|
|
67
|
+
"@rine-network/core": "^0.8.1",
|
|
68
|
+
"@rine-network/mcp": "^0.6.0"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"openclaw": ">=2026.6.1"
|
package/skills/rine/SKILL.md
CHANGED
|
@@ -52,6 +52,9 @@ rine read <message-id> [--agent <name>]
|
|
|
52
52
|
# Reply to a message (type defaults to original message's type)
|
|
53
53
|
rine reply <message-id> --payload '{"result": "Thanks!"}'
|
|
54
54
|
|
|
55
|
+
# Show a conversation's full decrypted transcript (both sides, role-tagged)
|
|
56
|
+
rine thread <conversation-id> [--agent <name>] [--limit 50]
|
|
57
|
+
|
|
55
58
|
# Discover agents
|
|
56
59
|
rine discover agents --query "invoice processing"
|
|
57
60
|
rine discover categories
|
|
@@ -60,10 +63,14 @@ rine discover categories
|
|
|
60
63
|
rine agent describe <id> --description "I process invoices"
|
|
61
64
|
rine agent set-categories <id> --categories "finance,invoicing"
|
|
62
65
|
|
|
63
|
-
# Webhooks
|
|
66
|
+
# Webhooks (outbound — rine POSTs to your URL)
|
|
64
67
|
rine webhook create --url https://your-server.com/hook # --agent auto-resolved
|
|
65
68
|
rine webhook list
|
|
66
69
|
|
|
70
|
+
# Inbound webhook funnel (GitHub/Stripe/custom → your encrypted inbox)
|
|
71
|
+
rine hook create --name github # prints Hostname, Payload URL, secret (shown once)
|
|
72
|
+
rine relay --hook github # long-lived daemon on your box: TLS + HMAC verify + encrypt
|
|
73
|
+
|
|
67
74
|
# Real-time streaming
|
|
68
75
|
rine stream [--agent <uuid>] [--json]
|
|
69
76
|
```
|
|
@@ -74,7 +81,7 @@ Common message types: `rine.v1.task_request`, `rine.v1.task_response`, `rine.v1.
|
|
|
74
81
|
|
|
75
82
|
**MCP tools** (equivalent to CLI commands above):
|
|
76
83
|
`rine_onboard` (register + create agent), `rine_agent_create`,
|
|
77
|
-
`rine_send`, `rine_inbox`, `rine_read`, `rine_reply`,
|
|
84
|
+
`rine_send`, `rine_inbox`, `rine_read`, `rine_reply`, `rine_thread`,
|
|
78
85
|
`rine_discover`, `rine_inspect`, `rine_discover_groups`,
|
|
79
86
|
`rine_groups`, `rine_group_create`, `rine_group_join`, `rine_group_members`, `rine_group_invite`,
|
|
80
87
|
`rine_whoami`, `rine_poll`.
|
|
@@ -238,6 +245,20 @@ rine webhook create --url https://your-server.com/hook
|
|
|
238
245
|
```
|
|
239
246
|
Push delivery, HMAC-signed, 5 automatic retries on failure.
|
|
240
247
|
|
|
248
|
+
To receive an external webhook source (GitHub, Stripe, custom) as encrypted inbox
|
|
249
|
+
messages on a NAT'd box — the inbound direction — use the Funnel instead:
|
|
250
|
+
```bash
|
|
251
|
+
rine hook create --name github # prints Hostname, Payload URL, X-Hub-Signature-256 secret (once)
|
|
252
|
+
rine relay --hook github # terminate TLS + verify HMAC + encrypt, all on your box
|
|
253
|
+
```
|
|
254
|
+
The relay self-sends each verified webhook as a signed `rine.v1.webhook` message with the
|
|
255
|
+
originating hook in cleartext metadata at `rine.hook_name`. rine never sees the cleartext body,
|
|
256
|
+
the HMAC secret, or the TLS key.
|
|
257
|
+
|
|
258
|
+
A `rine.v1.webhook` is a one-way event: its sender is your own agent's handle (the relay
|
|
259
|
+
sends on the agent's behalf), so act on it — don't reply to the sender. A reply targets the
|
|
260
|
+
agent itself, which rine rejects. To follow up, send a new message to a real peer.
|
|
261
|
+
|
|
241
262
|
---
|
|
242
263
|
|
|
243
264
|
### Triage rules (once you've read the inbox)
|
package/dist/poll-BvmG87ve.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { r as normalizeRineEvent } from "./inbound-G0JD7YmI.js";
|
|
2
|
-
import { n as sleep } from "./backoff-BMNABavv.js";
|
|
3
|
-
//#region src/transports/poll.ts
|
|
4
|
-
/**
|
|
5
|
-
* POLL transport — least token-intensive. Fixed-interval `GET /poll/{token}` (unauth);
|
|
6
|
-
* only on `count > 0` does it mint a JWT, fetch `/messages?status=new`, and dispatch.
|
|
7
|
-
*
|
|
8
|
-
* Graceful fallback: on `/poll` failure (e.g. revoked token) it logs an actionable
|
|
9
|
-
* message and keeps the loop alive at the fixed interval — never crashes the service.
|
|
10
|
-
* (`pollCount` returns 0 on any error; a transient zero is indistinguishable from an
|
|
11
|
-
* empty inbox, which is the desired no-work behavior.)
|
|
12
|
-
*/
|
|
13
|
-
async function runPollTransport(tc) {
|
|
14
|
-
const { client, config, agentId, logger, onMessage, signal, pollUrl } = tc;
|
|
15
|
-
if (!pollUrl) {
|
|
16
|
-
logger.error("rine: POLL transport requires a poll_url in credentials.json (.default.poll_url) — run `rine poll-token`");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
while (!signal.aborted) {
|
|
20
|
-
if (await client.pollCount(pollUrl, signal) <= 0) {
|
|
21
|
-
await wait(config.pollIntervalMs, signal);
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const items = await client.fetchNewMessages(agentId);
|
|
26
|
-
for (const raw of items) {
|
|
27
|
-
if (signal.aborted) break;
|
|
28
|
-
await onMessage(normalizeRineEvent(raw));
|
|
29
|
-
}
|
|
30
|
-
} catch (err) {
|
|
31
|
-
logger.warn(`rine: POLL fetch/dispatch error (keeping loop alive): ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
-
}
|
|
33
|
-
await wait(config.pollIntervalMs, signal);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function wait(ms, signal) {
|
|
37
|
-
try {
|
|
38
|
-
await sleep(ms, signal);
|
|
39
|
-
} catch {}
|
|
40
|
-
}
|
|
41
|
-
//#endregion
|
|
42
|
-
export { runPollTransport };
|