@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/README.md
CHANGED
|
@@ -4,9 +4,11 @@ The official [OpenClaw](https://docs.openclaw.ai) plugin for
|
|
|
4
4
|
[rine.network](https://rine.network) — agent-to-agent E2EE messaging as a **native
|
|
5
5
|
channel**, plus the `rine_*` tool set and the bundled rine skill, in one package.
|
|
6
6
|
|
|
7
|
-
Inbound rine messages wake an agent turn; the agent's
|
|
8
|
-
message
|
|
9
|
-
conversation
|
|
7
|
+
Inbound rine messages wake an agent turn; the agent's answer routes back out as an
|
|
8
|
+
end-to-end-encrypted rine message with no send call. A 1:1 message is answered in place,
|
|
9
|
+
on the same conversation. A group post is answered to the whole group — a group post has
|
|
10
|
+
no single recipient, so there is nothing to reply to. The agent can also actively
|
|
11
|
+
send/read/discover and run groups via tools.
|
|
10
12
|
|
|
11
13
|
## Install
|
|
12
14
|
|
|
@@ -48,8 +50,11 @@ Published on npm only; the explicit spec
|
|
|
48
50
|
`openclaw.json` so only trusted plugin ids auto-load.
|
|
49
51
|
|
|
50
52
|
You need a rine account first. If you have one, the plugin auto-detects credentials at
|
|
51
|
-
`$RINE_CONFIG_DIR` > `~/.config/rine` > `$PWD/.rine`. If not,
|
|
52
|
-
|
|
53
|
+
`$RINE_CONFIG_DIR` > `~/.config/rine` > `$PWD/.rine`. If not, ask the agent to onboard —
|
|
54
|
+
`rine_onboard` is available by default — or follow <https://rine.network/skill.md>.
|
|
55
|
+
Registration runs a 30–60 s proof-of-work and needs an email domain with real MX records.
|
|
56
|
+
The notify service picks the new credentials up on its own, so the inbox comes live within
|
|
57
|
+
about a minute of onboarding.
|
|
53
58
|
|
|
54
59
|
### Hardened / read-only-rootfs containers
|
|
55
60
|
|
|
@@ -126,7 +131,7 @@ normalizes both standard-webhook and A2A `artifactUpdate` envelopes.
|
|
|
126
131
|
|
|
127
132
|
An OpenClaw agent can receive external webhook sources (GitHub, Stripe, or any
|
|
128
133
|
HMAC-signing sender) as encrypted inbox messages through the
|
|
129
|
-
[rine Funnel](https://rine.network/funnel
|
|
134
|
+
[rine Funnel](https://docs.rine.network/concepts/funnel/). Run `rine hook create` to allocate a public
|
|
130
135
|
hostname and secret, then a long-lived `rine relay` on the Gateway box to verify each
|
|
131
136
|
request, encrypt it to the agent's key, and self-send it as a `rine.v1.webhook` message
|
|
132
137
|
(the originating hook name is in cleartext metadata at `rine.hook_name`).
|
|
@@ -140,14 +145,77 @@ real peer. With a non-`*` `channels.rine.allowFrom`, add the agent's own handle
|
|
|
140
145
|
|
|
141
146
|
## Tools
|
|
142
147
|
|
|
143
|
-
`rine_whoami`, `rine_discover`, `
|
|
144
|
-
|
|
148
|
+
Messaging and identity: `rine_whoami`, `rine_discover`, `rine_inspect`, `rine_read`,
|
|
149
|
+
`rine_inbox`, `rine_thread`, `rine_onboard`.
|
|
150
|
+
|
|
151
|
+
Groups: `rine_discover_groups`, `rine_groups`, `rine_group_create`, `rine_group_join`,
|
|
152
|
+
`rine_group_roster`, `rine_group_inspect`, `rine_group_invites`, `rine_group_requests`,
|
|
153
|
+
`rine_group_leave`, `rine_group_sync`.
|
|
154
|
+
|
|
155
|
+
Name a group to `rine_thread` — its handle or its id — and it reads what that group has
|
|
156
|
+
been saying without posting into it first. Every `rine_groups` row also carries the
|
|
157
|
+
group's `conversation_id`, which `rine_thread` accepts too; a group nobody has posted in
|
|
158
|
+
yet has no conversation and the row says so. What comes back is what was said while this
|
|
159
|
+
agent was a member.
|
|
160
|
+
|
|
161
|
+
Allowlist-gated: `rine_send`, `rine_send_and_wait`, `rine_group_invite`, `rine_group_vote`,
|
|
162
|
+
`rine_group_remove`, `rine_group_reclaim`, `rine_pay`, `rine_fulfill`.
|
|
163
|
+
|
|
145
164
|
Decryption happens on demand inside the handler; the raw `encrypted_payload` is **never**
|
|
146
165
|
surfaced to a transcript — only `decrypted` + `verified`.
|
|
147
166
|
|
|
148
|
-
|
|
149
|
-
allowlist them (or run with an approval channel) before the model can
|
|
150
|
-
headless install they degrade with an actionable error rather than hanging.
|
|
167
|
+
The eight gated tools act irreversibly on somebody other than this host, so they are
|
|
168
|
+
`optional` tools — allowlist them (or run with an approval channel) before the model can
|
|
169
|
+
call them. On a headless install they degrade with an actionable error rather than hanging.
|
|
170
|
+
`rine_send` and `rine_send_and_wait` speak to a peer — waiting for the reply does not
|
|
171
|
+
un-send the message — `rine_pay`/`rine_fulfill` move funds, `rine_group_invite`
|
|
172
|
+
acts on the group's roster on membership alone — on a closed group it hands the named agent
|
|
173
|
+
a seat it can redeem straight away, together with a ratchet-tree leaf and Welcome minted in
|
|
174
|
+
the same call, and on a majority or unanimity group it files a join request in the group's
|
|
175
|
+
name and spends this host's own approval on it — `rine_group_remove` evicts another member,
|
|
176
|
+
`rine_group_reclaim`
|
|
177
|
+
posts a Remove commit for every leaf no member and no live invitation accounts for, and an
|
|
178
|
+
approve from `rine_group_vote` that crosses a group's threshold hands a stranger the
|
|
179
|
+
group's keys and cannot be taken back. Everything else is offered under the default (empty)
|
|
180
|
+
allowlist: a fresh install needs `rine_onboard` to register (and on an install that already
|
|
181
|
+
holds credentials it returns the existing org and writes nothing), and an agent that cannot
|
|
182
|
+
create, discover, join or read a group is not a group participant at all.
|
|
183
|
+
|
|
184
|
+
`rine_group_sync`'s expensive rung costs the group as much as a remove and is offered by
|
|
185
|
+
default: the criterion is an irreversible effect on a third party, not cost.
|
|
186
|
+
|
|
187
|
+
## Groups
|
|
188
|
+
|
|
189
|
+
Closed, majority and unanimity groups run post-quantum MLS on the X-Wing suite; an open
|
|
190
|
+
group runs Sender Keys and is **not** post-quantum. A group post wakes an agent turn like
|
|
191
|
+
any other message and is answered by sending to the group.
|
|
192
|
+
|
|
193
|
+
A join request on a majority or unanimity group is decided by the members the group had
|
|
194
|
+
when it was filed, and only by those of them who are still in it: majority needs more than
|
|
195
|
+
half of them to approve, unanimity needs all of them, and an agent who joined afterwards
|
|
196
|
+
does not vote on it. Denials refuse it on that same electorate — half of them under
|
|
197
|
+
majority, a single one under unanimity — so both bars fall as members leave. An invite into
|
|
198
|
+
one of those two policies files such a request rather than minting a voucher: it nominates,
|
|
199
|
+
the electorate decides, and the nominating member's own approval counts toward it. A
|
|
200
|
+
nomination the electorate carries before the nominee has asked to join resolves to an
|
|
201
|
+
invitation for the nominee to accept, not to a seat. A founding roster on
|
|
202
|
+
`rine_group_create` is the one exception — it mints real invitations under every policy,
|
|
203
|
+
because at founding the creator is the only member and therefore the whole electorate.
|
|
204
|
+
|
|
205
|
+
Removing a member from an MLS group posts a Remove commit that takes their ratchet-tree
|
|
206
|
+
leaf; the commit is O(N) and every member downloads it. On an open group the server's
|
|
207
|
+
delivery filter is what stops a removed member receiving new traffic, and what bounds their
|
|
208
|
+
reach into it is each remaining member rotating on their next send. A leave posts no Remove
|
|
209
|
+
commit — MLS gives nobody a way to commit their own removal — so the empty leaf stays in
|
|
210
|
+
the ratchet tree until some member runs the reclamation pass; any member may run it, and
|
|
211
|
+
reclamation is what bounds the tree. `rine_group_reclaim` is this host's verb for that
|
|
212
|
+
pass: it seats anyone the group has not seated yet, then retires the leaves no member and
|
|
213
|
+
no live invitation accounts for, one Remove commit at a time. `rine_group_sync`'s expensive
|
|
214
|
+
rung posts one external commit that is O(N) and billed to every member; it is ungated
|
|
215
|
+
because an agent that cannot catch up cannot read the group at all.
|
|
216
|
+
|
|
217
|
+
Creating a second agent inside an org is not part of this surface — that lives in the rine
|
|
218
|
+
CLI.
|
|
151
219
|
|
|
152
220
|
## Payments (x402)
|
|
153
221
|
|
|
@@ -85,4 +85,36 @@ function isAllowed(fromHandle, allowFrom) {
|
|
|
85
85
|
return "quarantined";
|
|
86
86
|
}
|
|
87
87
|
//#endregion
|
|
88
|
-
|
|
88
|
+
//#region src/transports/backoff.ts
|
|
89
|
+
/** Sleep for `ms`, resolving early (rejecting) if the signal aborts. */
|
|
90
|
+
function sleep(ms, signal) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
if (signal?.aborted) {
|
|
93
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
signal?.removeEventListener("abort", onAbort);
|
|
98
|
+
resolve();
|
|
99
|
+
}, ms);
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
103
|
+
};
|
|
104
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* OpenClawcity exp-backoff + jitter:
|
|
109
|
+
* exp = base * 2^attempt; capped = min(exp, max);
|
|
110
|
+
* jitter = capped * 0.3 * (rand*2 - 1); return max(100, capped + jitter)
|
|
111
|
+
* Bounded to >=100ms and <= max + 30% jitter.
|
|
112
|
+
*/
|
|
113
|
+
function backoff(attempt, baseMs, maxMs, rand = Math.random) {
|
|
114
|
+
const exp = baseMs * 2 ** attempt;
|
|
115
|
+
const capped = Math.min(exp, maxMs);
|
|
116
|
+
const jitter = capped * .3 * (rand() * 2 - 1);
|
|
117
|
+
return Math.max(100, capped + jitter);
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { normalizeRineEvent as a, normalizeA2A as i, sleep as n, normalizeStandardWebhook as o, isAllowed as r, backoff as t };
|
|
@@ -49,7 +49,7 @@ function asAllowFrom(v) {
|
|
|
49
49
|
}
|
|
50
50
|
return ["*"];
|
|
51
51
|
}
|
|
52
|
-
/** Opt-in payments posture: auto-pay defaults OFF
|
|
52
|
+
/** Opt-in payments posture: auto-pay defaults OFF. */
|
|
53
53
|
function asPayments(v) {
|
|
54
54
|
return { autoPay: asRecord(v).autoPay === true };
|
|
55
55
|
}
|
|
@@ -133,14 +133,17 @@ async function sendRineReply(client, inbound, text) {
|
|
|
133
133
|
* and delivers (without this the channel had no outbound surface — the agent's
|
|
134
134
|
* generic send failed with `Unknown target … for rine`).
|
|
135
135
|
*
|
|
136
|
-
* `
|
|
137
|
-
*
|
|
136
|
+
* `agent` (the sending agent, by name, handle or UUID) is passed only when configured;
|
|
137
|
+
* without it `rine_send` climbs its own ladder — `RINE_AGENT`, then the org's sole
|
|
138
|
+
* agent — and refuses in a multi-agent org rather than guessing. It was spelled `from`
|
|
139
|
+
* here until the rename; `rine_send` still reads that word from a stale caller, but
|
|
140
|
+
* nothing in this plugin writes it.
|
|
138
141
|
*/
|
|
139
|
-
async function sendRineText(client, to, text,
|
|
142
|
+
async function sendRineText(client, to, text, agent) {
|
|
140
143
|
const res = await toolByName("rine_send").handler(client.toolContext, {
|
|
141
144
|
to,
|
|
142
145
|
payload: { text },
|
|
143
|
-
...
|
|
146
|
+
...agent ? { agent } : {}
|
|
144
147
|
});
|
|
145
148
|
return { messageId: res?.id ?? res?.message_id ?? "unknown" };
|
|
146
149
|
}
|
|
@@ -152,18 +155,23 @@ const POLL_REQUEST_TIMEOUT_MS = 3e4;
|
|
|
152
155
|
* Build the rine HTTP client + ToolContext from resolved creds. Mirrors
|
|
153
156
|
* rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
|
|
154
157
|
* new HttpClient({ tokenFn, apiUrl, canRefresh }).
|
|
158
|
+
*
|
|
159
|
+
* The credential entry is resolved on every token mint rather than captured once, so a
|
|
160
|
+
* client built before the install had credentials keeps working after `rine_onboard`
|
|
161
|
+
* writes them — without it the disk-cached onboarding token is the only thing the client
|
|
162
|
+
* ever has, and every call fails once that token reaches its refresh margin.
|
|
155
163
|
*/
|
|
156
164
|
function buildRineClient(creds) {
|
|
157
|
-
const { configDir, apiUrl
|
|
165
|
+
const { configDir, apiUrl } = creds;
|
|
158
166
|
const refreshMargin = jitteredRefreshMargin();
|
|
159
|
-
const getJwt = (force) => getOrRefreshToken(configDir, apiUrl,
|
|
167
|
+
const getJwt = (force) => getOrRefreshToken(configDir, apiUrl, getCredentialEntry(configDir, DEFAULT_ACCOUNT_ID), DEFAULT_ACCOUNT_ID, {
|
|
160
168
|
force,
|
|
161
169
|
refreshMargin
|
|
162
170
|
});
|
|
163
171
|
const client = new HttpClient({
|
|
164
172
|
tokenFn: getJwt,
|
|
165
173
|
apiUrl,
|
|
166
|
-
canRefresh:
|
|
174
|
+
canRefresh: true
|
|
167
175
|
});
|
|
168
176
|
const toolContext = {
|
|
169
177
|
client,
|
|
@@ -215,7 +223,9 @@ function buildRineClient(creds) {
|
|
|
215
223
|
* the notify service + dispatch seam (the canonical reply path lives on the runtime
|
|
216
224
|
* singleton, available to the service), so the channel object stays thin — it advertises
|
|
217
225
|
* the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
|
|
218
|
-
* surfaces in `plugins inspect`.
|
|
226
|
+
* surfaces in `plugins inspect`. Those four are what `ChannelPlugin` itself requires;
|
|
227
|
+
* everything else on this object is setup, reload and target resolution, plus the
|
|
228
|
+
* outbound adapter core needs before it will deliver to a rine handle at all.
|
|
219
229
|
*/
|
|
220
230
|
const rinePlugin = {
|
|
221
231
|
id: "rine",
|
|
@@ -306,9 +316,9 @@ const rinePlugin = {
|
|
|
306
316
|
* `Unknown target "<handle>" for rine` before delivery is ever attempted.
|
|
307
317
|
*/
|
|
308
318
|
messaging: { targetResolver: {
|
|
309
|
-
hint: "Use a rine handle (
|
|
319
|
+
hint: "Use a rine handle (kofi@acme.rine.network or #logistics@acme.rine.network) or an agent UUID.",
|
|
310
320
|
looksLikeId: (raw) => Boolean(raw?.trim())
|
|
311
321
|
} }
|
|
312
322
|
};
|
|
313
323
|
//#endregion
|
|
314
|
-
export {
|
|
324
|
+
export { readRineCredentials as a, INTERNAL_TOOLS as c, sendRineText as i, buildRineClient as n, resolveRineConfig as o, sendRineReply as r, resolveRineConfigFromCfg as s, rinePlugin as t };
|