@rine-network/openclaw 0.1.4 → 0.1.5
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 +28 -1
- package/dist/channel-DpHhVQ-n.js +277 -0
- package/dist/index.js +12 -77
- package/dist/setup.js +1 -1
- package/dist/src/constants.d.ts +3 -2
- package/dist/src/dispatch.d.ts +8 -1
- package/dist/src/outbound.d.ts +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/config-BsdV6THh.js +0 -113
package/README.md
CHANGED
|
@@ -11,8 +11,33 @@ conversation). The agent can also actively send/read/discover via tools.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
+
# 1. Install and enable the plugin
|
|
14
15
|
openclaw plugins install npm:@rine-network/openclaw
|
|
15
16
|
openclaw plugins enable rine
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Step 2 is required — add the `channels.rine` block to `openclaw.json`:**
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"channels": {
|
|
24
|
+
"rine": {
|
|
25
|
+
"transport": "sse",
|
|
26
|
+
"healthMonitor": { "enabled": false }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
OpenClaw only activates a channel plugin — importing its code, registering the notify
|
|
33
|
+
service, tools, and inbound route — when the channel id appears under `channels.<id>` in
|
|
34
|
+
`openclaw.json`. Without it, `plugins list` shows the plugin as "enabled/loaded" but the
|
|
35
|
+
inbox is **silently dead** (no notify service) and you will see recurring
|
|
36
|
+
`health-monitor: restarting (reason: stopped)` churn. Setting
|
|
37
|
+
`healthMonitor.enabled: false` silences that churn (requires plugin ≥ 0.1.3).
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# 3. Restart and verify
|
|
16
41
|
openclaw gateway restart
|
|
17
42
|
openclaw plugins inspect rine --runtime --json # verify channel + tools + service + route
|
|
18
43
|
```
|
|
@@ -53,7 +78,9 @@ install/update time — once installed, the plugin loads normally. (npm placing
|
|
|
53
78
|
|
|
54
79
|
## Pick a transport posture
|
|
55
80
|
|
|
56
|
-
|
|
81
|
+
The `channels.rine` block in `openclaw.json` is what activates the channel (see **Install**
|
|
82
|
+
above). Once you have the block, you can tune the `transport` field within it — the default
|
|
83
|
+
is `sse` if you omit the field entirely:
|
|
57
84
|
|
|
58
85
|
| Transport | How it works | Best for |
|
|
59
86
|
|-----------|--------------|----------|
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { patchTopLevelChannelConfigSection } from "openclaw/plugin-sdk/setup";
|
|
2
|
+
import { HttpClient, getCredentialEntry, getOrRefreshToken, resolveApiUrl, resolveConfigDir } from "@rine-network/core";
|
|
3
|
+
import { tools } from "@rine-network/mcp/tools";
|
|
4
|
+
//#region src/constants.ts
|
|
5
|
+
/**
|
|
6
|
+
* The single `"default"` profile/account literal, shared so a rename touches one place.
|
|
7
|
+
*
|
|
8
|
+
* It is the rine credential *profile* key passed to core's
|
|
9
|
+
* `getCredentialEntry`/`getOrRefreshToken` (the entry under `.default` in
|
|
10
|
+
* credentials.json) and, identically, the OpenClaw single-account id for the rine
|
|
11
|
+
* channel. Both are `"default"` by convention.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
14
|
+
/**
|
|
15
|
+
* Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
|
|
16
|
+
* Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
|
|
17
|
+
* fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path;
|
|
18
|
+
* `rine_send` backs the channel `outbound` adapter (active sends to an arbitrary target).
|
|
19
|
+
*/
|
|
20
|
+
const INTERNAL_TOOLS = ["rine_reply", "rine_send"];
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/config.ts
|
|
23
|
+
const DEFAULTS = {
|
|
24
|
+
transport: "sse",
|
|
25
|
+
pollIntervalMs: 6e4,
|
|
26
|
+
reconnectBaseMs: 3e3,
|
|
27
|
+
reconnectMaxMs: 3e5,
|
|
28
|
+
a2aAcceptCleartext: true
|
|
29
|
+
};
|
|
30
|
+
function asString(v) {
|
|
31
|
+
return typeof v === "string" && v.trim() !== "" ? v : void 0;
|
|
32
|
+
}
|
|
33
|
+
function asNumber(v, fallback) {
|
|
34
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
35
|
+
}
|
|
36
|
+
function asTransport(v) {
|
|
37
|
+
return v === "expose" || v === "poll" || v === "sse" ? v : DEFAULTS.transport;
|
|
38
|
+
}
|
|
39
|
+
function asAllowFrom(v) {
|
|
40
|
+
if (Array.isArray(v)) {
|
|
41
|
+
const entries = v.filter((e) => typeof e === "string");
|
|
42
|
+
return entries.length > 0 ? entries : ["*"];
|
|
43
|
+
}
|
|
44
|
+
return ["*"];
|
|
45
|
+
}
|
|
46
|
+
/** Resolve the typed config from a raw `api.pluginConfig` (or `{}`), applying defaults. */
|
|
47
|
+
function resolveRineConfig(raw = {}) {
|
|
48
|
+
return {
|
|
49
|
+
transport: asTransport(raw.transport),
|
|
50
|
+
configDir: asString(raw.configDir),
|
|
51
|
+
agentId: asString(raw.agentId),
|
|
52
|
+
baseUrl: asString(raw.baseUrl),
|
|
53
|
+
pollIntervalMs: asNumber(raw.pollIntervalMs, DEFAULTS.pollIntervalMs),
|
|
54
|
+
reconnectBaseMs: asNumber(raw.reconnectBaseMs, DEFAULTS.reconnectBaseMs),
|
|
55
|
+
reconnectMaxMs: asNumber(raw.reconnectMaxMs, DEFAULTS.reconnectMaxMs),
|
|
56
|
+
exposeBaseUrl: asString(raw.exposeBaseUrl),
|
|
57
|
+
a2aAcceptCleartext: typeof raw.a2aAcceptCleartext === "boolean" ? raw.a2aAcceptCleartext : DEFAULTS.a2aAcceptCleartext,
|
|
58
|
+
allowFrom: asAllowFrom(raw.allowFrom)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve rine credentials using core's 3-level config-dir fallback
|
|
63
|
+
* ($RINE_CONFIG_DIR > ~/.config/rine > $PWD/.rine). An explicit `cfg.configDir`
|
|
64
|
+
* override wins and is threaded directly — we never mutate `process.env`. Reuses core
|
|
65
|
+
* helpers; no host keychain access, no token writes.
|
|
66
|
+
*/
|
|
67
|
+
function readRineCredentials(cfg) {
|
|
68
|
+
const configDir = cfg.configDir ?? resolveConfigDir();
|
|
69
|
+
const apiUrl = cfg.baseUrl ?? resolveApiUrl();
|
|
70
|
+
const entry = getCredentialEntry(configDir, DEFAULT_ACCOUNT_ID);
|
|
71
|
+
return {
|
|
72
|
+
configDir,
|
|
73
|
+
apiUrl,
|
|
74
|
+
entry,
|
|
75
|
+
pollUrl: entry?.poll_url
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/outbound.ts
|
|
80
|
+
function toolByName(name, all = tools) {
|
|
81
|
+
const def = all.find((t) => t.name === name);
|
|
82
|
+
if (!def) throw new Error(`rine: required mcp tool "${name}" not found — version skew`);
|
|
83
|
+
return def;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Route an agent reply back out as a rine message, reusing the lifted `rine_reply`
|
|
87
|
+
* handler (E2EE encrypt + POST /messages/{id}/reply, auto-routed to the original
|
|
88
|
+
* sender, conversation_id preserved). No crypto/HTTP reimplemented.
|
|
89
|
+
*/
|
|
90
|
+
async function sendRineReply(client, inbound, text) {
|
|
91
|
+
return toolByName("rine_reply").handler(client.toolContext, {
|
|
92
|
+
message_id: inbound.id,
|
|
93
|
+
payload: { text }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Active send of an agent reply/message to an arbitrary rine target, reusing the
|
|
98
|
+
* lifted `rine_send` handler (E2EE encrypt + POST /messages). Backs the channel
|
|
99
|
+
* `outbound.sendText` adapter so OpenClaw's generic `send` message-action resolves
|
|
100
|
+
* and delivers (without this the channel had no outbound surface — the agent's
|
|
101
|
+
* generic send failed with `Unknown target … for rine`).
|
|
102
|
+
*
|
|
103
|
+
* `from` (the sending agent id) is passed only when configured; in a single-agent
|
|
104
|
+
* org `rine_send` resolves the sole agent itself.
|
|
105
|
+
*/
|
|
106
|
+
async function sendRineText(client, to, text, from) {
|
|
107
|
+
const res = await toolByName("rine_send").handler(client.toolContext, {
|
|
108
|
+
to,
|
|
109
|
+
payload: { text },
|
|
110
|
+
...from ? { from } : {}
|
|
111
|
+
});
|
|
112
|
+
return { messageId: res?.id ?? res?.message_id ?? "unknown" };
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/rine-client.ts
|
|
116
|
+
/** Hard ceiling on a single unauthenticated /poll request (the poll loop owns the cadence). */
|
|
117
|
+
const POLL_REQUEST_TIMEOUT_MS = 3e4;
|
|
118
|
+
/**
|
|
119
|
+
* Build the rine HTTP client + ToolContext from resolved creds. Mirrors
|
|
120
|
+
* rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
|
|
121
|
+
* new HttpClient({ tokenFn, apiUrl, canRefresh }).
|
|
122
|
+
*/
|
|
123
|
+
function buildRineClient(creds) {
|
|
124
|
+
const { configDir, apiUrl, entry } = creds;
|
|
125
|
+
const getJwt = (force) => getOrRefreshToken(configDir, apiUrl, entry, DEFAULT_ACCOUNT_ID, { force });
|
|
126
|
+
const client = new HttpClient({
|
|
127
|
+
tokenFn: getJwt,
|
|
128
|
+
apiUrl,
|
|
129
|
+
canRefresh: Boolean(entry)
|
|
130
|
+
});
|
|
131
|
+
const toolContext = {
|
|
132
|
+
client,
|
|
133
|
+
configDir,
|
|
134
|
+
apiUrl
|
|
135
|
+
};
|
|
136
|
+
async function pollCount(pollUrl, signal) {
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(pollUrl, { signal: signal ?? AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS) });
|
|
139
|
+
if (!res.ok) return 0;
|
|
140
|
+
const body = await res.json();
|
|
141
|
+
return typeof body.count === "number" ? body.count : 0;
|
|
142
|
+
} catch {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function fetchNewMessages(agentId, limit = 50) {
|
|
147
|
+
const res = await client.get(`/agents/${agentId}/messages`, {
|
|
148
|
+
status: "new",
|
|
149
|
+
limit
|
|
150
|
+
});
|
|
151
|
+
return Array.isArray(res.items) ? res.items : [];
|
|
152
|
+
}
|
|
153
|
+
async function markDelivered(agentId, ids) {
|
|
154
|
+
if (ids.length === 0) return 0;
|
|
155
|
+
return (await client.markDelivered(agentId, ids)).marked;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
toolContext,
|
|
159
|
+
client,
|
|
160
|
+
configDir,
|
|
161
|
+
apiUrl,
|
|
162
|
+
getJwt,
|
|
163
|
+
pollCount,
|
|
164
|
+
fetchNewMessages,
|
|
165
|
+
markDelivered
|
|
166
|
+
};
|
|
167
|
+
}
|
|
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
|
+
/**
|
|
176
|
+
* Minimal but type-valid `ChannelPlugin` for rine. Required fields only:
|
|
177
|
+
* id / meta / capabilities / config. Inbound delivery + outbound replies are owned by
|
|
178
|
+
* the notify service + dispatch seam (the canonical reply path lives on the runtime
|
|
179
|
+
* singleton, available to the service), so the channel object stays thin — it advertises
|
|
180
|
+
* the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
|
|
181
|
+
* surfaces in `plugins inspect`. See SDK_CONTRACT.md.
|
|
182
|
+
*/
|
|
183
|
+
const rinePlugin = {
|
|
184
|
+
id: "rine",
|
|
185
|
+
meta: {
|
|
186
|
+
id: "rine",
|
|
187
|
+
label: "rine",
|
|
188
|
+
selectionLabel: "rine",
|
|
189
|
+
detailLabel: "rine network",
|
|
190
|
+
docsPath: "/channels/rine",
|
|
191
|
+
docsLabel: "rine",
|
|
192
|
+
blurb: "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
|
|
193
|
+
systemImage: "antenna.radiowaves.left.and.right",
|
|
194
|
+
order: 120,
|
|
195
|
+
showConfigured: true
|
|
196
|
+
},
|
|
197
|
+
capabilities: {
|
|
198
|
+
chatTypes: ["direct", "group"],
|
|
199
|
+
reply: true,
|
|
200
|
+
threads: true,
|
|
201
|
+
media: false
|
|
202
|
+
},
|
|
203
|
+
reload: { configPrefixes: ["channels.rine"] },
|
|
204
|
+
config: {
|
|
205
|
+
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
206
|
+
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
|
207
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID
|
|
208
|
+
},
|
|
209
|
+
/**
|
|
210
|
+
* Setup wizard: scaffolds a default `channels.rine` block on first run so
|
|
211
|
+
* users get a working channel without hand-editing openclaw.json.
|
|
212
|
+
* Rine credentials live in ~/.config/rine (not openclaw.json), so
|
|
213
|
+
* `credentials: []` — the wizard completes immediately after the prepare hook.
|
|
214
|
+
*/
|
|
215
|
+
setupWizard: {
|
|
216
|
+
channel: "rine",
|
|
217
|
+
status: {
|
|
218
|
+
configuredLabel: "rine configured",
|
|
219
|
+
unconfiguredLabel: "rine not configured",
|
|
220
|
+
configuredHint: "Receiving messages via rine network",
|
|
221
|
+
unconfiguredHint: "Run 'openclaw plugins setup rine' to activate",
|
|
222
|
+
resolveConfigured: ({ cfg }) => {
|
|
223
|
+
const ch = cfg.channels;
|
|
224
|
+
return typeof ch?.rine === "object" && ch.rine !== null;
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
credentials: [],
|
|
228
|
+
prepare: ({ cfg }) => {
|
|
229
|
+
const ch = cfg.channels;
|
|
230
|
+
if (typeof ch?.rine === "object" && ch.rine !== null) return;
|
|
231
|
+
return { cfg: patchTopLevelChannelConfigSection({
|
|
232
|
+
cfg,
|
|
233
|
+
channel: "rine",
|
|
234
|
+
patch: {
|
|
235
|
+
transport: "sse",
|
|
236
|
+
healthMonitor: { enabled: false }
|
|
237
|
+
}
|
|
238
|
+
}) };
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
outbound: {
|
|
242
|
+
deliveryMode: "direct",
|
|
243
|
+
resolveTarget: ({ to }) => {
|
|
244
|
+
const trimmed = to?.trim();
|
|
245
|
+
if (!trimmed) return {
|
|
246
|
+
ok: false,
|
|
247
|
+
error: /* @__PURE__ */ new Error("Delivering to rine needs a target handle or UUID (e.g. alice@acme or #ops@acme).")
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
to: trimmed
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
sendText: async (ctx) => {
|
|
255
|
+
const config = rineConfigFromCfg(ctx.cfg);
|
|
256
|
+
const creds = readRineCredentials(config);
|
|
257
|
+
if (!creds.entry) throw new Error("rine: no credentials at the resolved config dir — run rine_onboard or set RINE_CONFIG_DIR.");
|
|
258
|
+
const { messageId } = await sendRineText(buildRineClient(creds), ctx.to, ctx.text, config.agentId);
|
|
259
|
+
return {
|
|
260
|
+
channel: "rine",
|
|
261
|
+
messageId
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
/**
|
|
266
|
+
* Tell core that any non-empty string is a valid rine target. Without this,
|
|
267
|
+
* core's directory-search path treats a free-form `name@org` handle as a
|
|
268
|
+
* display-name (it matches none of the built-in id heuristics) and emits
|
|
269
|
+
* `Unknown target "<handle>" for rine` before delivery is ever attempted.
|
|
270
|
+
*/
|
|
271
|
+
messaging: { targetResolver: {
|
|
272
|
+
hint: "Use a rine handle (name@org or #group@org) or an agent UUID.",
|
|
273
|
+
looksLikeId: (raw) => Boolean(raw?.trim())
|
|
274
|
+
} }
|
|
275
|
+
};
|
|
276
|
+
//#endregion
|
|
277
|
+
export { resolveRineConfig as a, readRineCredentials as i, buildRineClient as n, INTERNAL_TOOLS as o, sendRineReply as r, rinePlugin as t };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as resolveRineConfig, i as readRineCredentials, n as buildRineClient, o as INTERNAL_TOOLS, r as sendRineReply, t as rinePlugin } from "./channel-DpHhVQ-n.js";
|
|
2
2
|
import { i as normalizeStandardWebhook, n as normalizeA2A, t as isAllowed } from "./inbound-G0JD7YmI.js";
|
|
3
3
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
|
|
4
|
-
import {
|
|
5
|
-
import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/channel-inbound";
|
|
4
|
+
import { fetchAgents, resolveAgent } from "@rine-network/core";
|
|
6
5
|
import { tools } from "@rine-network/mcp/tools";
|
|
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
|
|
9
9
|
var description = "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).";
|
|
@@ -84,85 +84,20 @@ var configSchema = {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
//#endregion
|
|
87
|
-
//#region src/rine-client.ts
|
|
88
|
-
/** Hard ceiling on a single unauthenticated /poll request (the poll loop owns the cadence). */
|
|
89
|
-
const POLL_REQUEST_TIMEOUT_MS = 3e4;
|
|
90
|
-
/**
|
|
91
|
-
* Build the rine HTTP client + ToolContext from resolved creds. Mirrors
|
|
92
|
-
* rine-mcp/src/server.ts bootstrap: tokenFn = getCredentialEntry + getOrRefreshToken;
|
|
93
|
-
* new HttpClient({ tokenFn, apiUrl, canRefresh }).
|
|
94
|
-
*/
|
|
95
|
-
function buildRineClient(creds) {
|
|
96
|
-
const { configDir, apiUrl, entry } = creds;
|
|
97
|
-
const getJwt = (force) => getOrRefreshToken(configDir, apiUrl, entry, DEFAULT_ACCOUNT_ID, { force });
|
|
98
|
-
const client = new HttpClient({
|
|
99
|
-
tokenFn: getJwt,
|
|
100
|
-
apiUrl,
|
|
101
|
-
canRefresh: Boolean(entry)
|
|
102
|
-
});
|
|
103
|
-
const toolContext = {
|
|
104
|
-
client,
|
|
105
|
-
configDir,
|
|
106
|
-
apiUrl
|
|
107
|
-
};
|
|
108
|
-
async function pollCount(pollUrl, signal) {
|
|
109
|
-
try {
|
|
110
|
-
const res = await fetch(pollUrl, { signal: signal ?? AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS) });
|
|
111
|
-
if (!res.ok) return 0;
|
|
112
|
-
const body = await res.json();
|
|
113
|
-
return typeof body.count === "number" ? body.count : 0;
|
|
114
|
-
} catch {
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async function fetchNewMessages(agentId, limit = 50) {
|
|
119
|
-
const res = await client.get(`/agents/${agentId}/messages`, {
|
|
120
|
-
status: "new",
|
|
121
|
-
limit
|
|
122
|
-
});
|
|
123
|
-
return Array.isArray(res.items) ? res.items : [];
|
|
124
|
-
}
|
|
125
|
-
async function markDelivered(agentId, ids) {
|
|
126
|
-
if (ids.length === 0) return 0;
|
|
127
|
-
return (await client.markDelivered(agentId, ids)).marked;
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
toolContext,
|
|
131
|
-
client,
|
|
132
|
-
configDir,
|
|
133
|
-
apiUrl,
|
|
134
|
-
getJwt,
|
|
135
|
-
pollCount,
|
|
136
|
-
fetchNewMessages,
|
|
137
|
-
markDelivered
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/outbound.ts
|
|
142
|
-
function toolByName(name, all = tools) {
|
|
143
|
-
const def = all.find((t) => t.name === name);
|
|
144
|
-
if (!def) throw new Error(`rine: required mcp tool "${name}" not found — version skew`);
|
|
145
|
-
return def;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Route an agent reply back out as a rine message, reusing the lifted `rine_reply`
|
|
149
|
-
* handler (E2EE encrypt + POST /messages/{id}/reply, auto-routed to the original
|
|
150
|
-
* sender, conversation_id preserved). No crypto/HTTP reimplemented.
|
|
151
|
-
*/
|
|
152
|
-
async function sendRineReply(client, inbound, text) {
|
|
153
|
-
return toolByName("rine_reply").handler(client.toolContext, {
|
|
154
|
-
message_id: inbound.id,
|
|
155
|
-
payload: { text }
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
//#endregion
|
|
159
87
|
//#region src/dispatch.ts
|
|
160
88
|
function msgOf(err) {
|
|
161
89
|
return err instanceof Error ? err.message : String(err);
|
|
162
90
|
}
|
|
163
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
|
|
93
|
+
*
|
|
94
|
+
* The body also binds the reply target to the SENDER. Without this, a model that replies via the
|
|
95
|
+
* optional `rine_send` tool can copy the message's own `to_agent_id` (= this agent's id, surfaced
|
|
96
|
+
* by `rine_read`) and self-target, producing `Unknown target` (TODO-4). The auto-routing path is
|
|
97
|
+
* `rine_reply message_id=<id>`, which derives the recipient from the original sender server-side.
|
|
98
|
+
*/
|
|
164
99
|
function pointerText(msg) {
|
|
165
|
-
return `[rine ${msg.type} from ${msg.fromHandle}] message ${msg.id} (read with rine_read)`;
|
|
100
|
+
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.)`;
|
|
166
101
|
}
|
|
167
102
|
/**
|
|
168
103
|
* Build the shared `onMessage` path all transports converge on.
|
package/dist/setup.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as resolveRineConfig, i as readRineCredentials, t as rinePlugin } from "./channel-DpHhVQ-n.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/constants.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const DEFAULT_ACCOUNT_ID = "default";
|
|
|
10
10
|
/**
|
|
11
11
|
* Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
|
|
12
12
|
* Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
|
|
13
|
-
* fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path
|
|
13
|
+
* fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path;
|
|
14
|
+
* `rine_send` backs the channel `outbound` adapter (active sends to an arbitrary target).
|
|
14
15
|
*/
|
|
15
|
-
export declare const INTERNAL_TOOLS: readonly ["rine_reply"];
|
|
16
|
+
export declare const INTERNAL_TOOLS: readonly ["rine_reply", "rine_send"];
|
package/dist/src/dispatch.d.ts
CHANGED
|
@@ -24,7 +24,14 @@ export interface OnMessageDeps {
|
|
|
24
24
|
* a transient error that must be retried, so the cursor must stay behind it.
|
|
25
25
|
*/
|
|
26
26
|
export type OnMessage = (msg: RineInbound) => Promise<boolean>;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`.
|
|
29
|
+
*
|
|
30
|
+
* The body also binds the reply target to the SENDER. Without this, a model that replies via the
|
|
31
|
+
* optional `rine_send` tool can copy the message's own `to_agent_id` (= this agent's id, surfaced
|
|
32
|
+
* by `rine_read`) and self-target, producing `Unknown target` (TODO-4). The auto-routing path is
|
|
33
|
+
* `rine_reply message_id=<id>`, which derives the recipient from the original sender server-side.
|
|
34
|
+
*/
|
|
28
35
|
export declare function pointerText(msg: RineInbound): string;
|
|
29
36
|
/**
|
|
30
37
|
* Build the shared `onMessage` path all transports converge on.
|
package/dist/src/outbound.d.ts
CHANGED
|
@@ -6,3 +6,16 @@ import type { RineInbound } from "./types.js";
|
|
|
6
6
|
* sender, conversation_id preserved). No crypto/HTTP reimplemented.
|
|
7
7
|
*/
|
|
8
8
|
export declare function sendRineReply(client: RineClient, inbound: RineInbound, text: string): Promise<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Active send of an agent reply/message to an arbitrary rine target, reusing the
|
|
11
|
+
* lifted `rine_send` handler (E2EE encrypt + POST /messages). Backs the channel
|
|
12
|
+
* `outbound.sendText` adapter so OpenClaw's generic `send` message-action resolves
|
|
13
|
+
* and delivers (without this the channel had no outbound surface — the agent's
|
|
14
|
+
* generic send failed with `Unknown target … for rine`).
|
|
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.
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendRineText(client: RineClient, to: string, text: string, from?: string): Promise<{
|
|
20
|
+
messageId: string;
|
|
21
|
+
}>;
|
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.1.5",
|
|
7
7
|
"channels": ["rine"],
|
|
8
8
|
"skills": ["skills/rine"],
|
|
9
9
|
"activation": { "onStartup": true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rine-network/openclaw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
package/dist/config-BsdV6THh.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { getCredentialEntry, resolveApiUrl, resolveConfigDir } from "@rine-network/core";
|
|
2
|
-
//#region src/constants.ts
|
|
3
|
-
/**
|
|
4
|
-
* The single `"default"` profile/account literal, shared so a rename touches one place.
|
|
5
|
-
*
|
|
6
|
-
* It is the rine credential *profile* key passed to core's
|
|
7
|
-
* `getCredentialEntry`/`getOrRefreshToken` (the entry under `.default` in
|
|
8
|
-
* credentials.json) and, identically, the OpenClaw single-account id for the rine
|
|
9
|
-
* channel. Both are `"default"` by convention.
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_ACCOUNT_ID = "default";
|
|
12
|
-
/**
|
|
13
|
-
* Internal mcp tools the plugin depends on at runtime but does NOT expose to the agent.
|
|
14
|
-
* Validated at startup (alongside `selectExposedTools`) so a rename of an mcp tool fails
|
|
15
|
-
* fast at load, not at first reply. `rine_reply` backs the inbound→reply dispatch path.
|
|
16
|
-
*/
|
|
17
|
-
const INTERNAL_TOOLS = ["rine_reply"];
|
|
18
|
-
//#endregion
|
|
19
|
-
//#region src/channel.ts
|
|
20
|
-
/**
|
|
21
|
-
* Minimal but type-valid `ChannelPlugin` for rine. Required fields only:
|
|
22
|
-
* id / meta / capabilities / config. Inbound delivery + outbound replies are owned by
|
|
23
|
-
* the notify service + dispatch seam (the canonical reply path lives on the runtime
|
|
24
|
-
* singleton, available to the service), so the channel object stays thin — it advertises
|
|
25
|
-
* the `rine` channel so sessions key as `agent:<id>:rine:<kind>:<peer>` and the channel
|
|
26
|
-
* surfaces in `plugins inspect`. See SDK_CONTRACT.md.
|
|
27
|
-
*/
|
|
28
|
-
const rinePlugin = {
|
|
29
|
-
id: "rine",
|
|
30
|
-
meta: {
|
|
31
|
-
id: "rine",
|
|
32
|
-
label: "rine",
|
|
33
|
-
selectionLabel: "rine",
|
|
34
|
-
detailLabel: "rine network",
|
|
35
|
-
docsPath: "/channels/rine",
|
|
36
|
-
docsLabel: "rine",
|
|
37
|
-
blurb: "Agent-to-agent E2EE messaging over the rine network (A2A relay / SSE / poll).",
|
|
38
|
-
systemImage: "antenna.radiowaves.left.and.right",
|
|
39
|
-
order: 120,
|
|
40
|
-
showConfigured: true
|
|
41
|
-
},
|
|
42
|
-
capabilities: {
|
|
43
|
-
chatTypes: ["direct", "group"],
|
|
44
|
-
reply: true,
|
|
45
|
-
threads: true,
|
|
46
|
-
media: false
|
|
47
|
-
},
|
|
48
|
-
reload: { configPrefixes: ["channels.rine"] },
|
|
49
|
-
config: {
|
|
50
|
-
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
51
|
-
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "default" }),
|
|
52
|
-
defaultAccountId: () => DEFAULT_ACCOUNT_ID
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
//#endregion
|
|
56
|
-
//#region src/config.ts
|
|
57
|
-
const DEFAULTS = {
|
|
58
|
-
transport: "sse",
|
|
59
|
-
pollIntervalMs: 6e4,
|
|
60
|
-
reconnectBaseMs: 3e3,
|
|
61
|
-
reconnectMaxMs: 3e5,
|
|
62
|
-
a2aAcceptCleartext: true
|
|
63
|
-
};
|
|
64
|
-
function asString(v) {
|
|
65
|
-
return typeof v === "string" && v.trim() !== "" ? v : void 0;
|
|
66
|
-
}
|
|
67
|
-
function asNumber(v, fallback) {
|
|
68
|
-
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
69
|
-
}
|
|
70
|
-
function asTransport(v) {
|
|
71
|
-
return v === "expose" || v === "poll" || v === "sse" ? v : DEFAULTS.transport;
|
|
72
|
-
}
|
|
73
|
-
function asAllowFrom(v) {
|
|
74
|
-
if (Array.isArray(v)) {
|
|
75
|
-
const entries = v.filter((e) => typeof e === "string");
|
|
76
|
-
return entries.length > 0 ? entries : ["*"];
|
|
77
|
-
}
|
|
78
|
-
return ["*"];
|
|
79
|
-
}
|
|
80
|
-
/** Resolve the typed config from a raw `api.pluginConfig` (or `{}`), applying defaults. */
|
|
81
|
-
function resolveRineConfig(raw = {}) {
|
|
82
|
-
return {
|
|
83
|
-
transport: asTransport(raw.transport),
|
|
84
|
-
configDir: asString(raw.configDir),
|
|
85
|
-
agentId: asString(raw.agentId),
|
|
86
|
-
baseUrl: asString(raw.baseUrl),
|
|
87
|
-
pollIntervalMs: asNumber(raw.pollIntervalMs, DEFAULTS.pollIntervalMs),
|
|
88
|
-
reconnectBaseMs: asNumber(raw.reconnectBaseMs, DEFAULTS.reconnectBaseMs),
|
|
89
|
-
reconnectMaxMs: asNumber(raw.reconnectMaxMs, DEFAULTS.reconnectMaxMs),
|
|
90
|
-
exposeBaseUrl: asString(raw.exposeBaseUrl),
|
|
91
|
-
a2aAcceptCleartext: typeof raw.a2aAcceptCleartext === "boolean" ? raw.a2aAcceptCleartext : DEFAULTS.a2aAcceptCleartext,
|
|
92
|
-
allowFrom: asAllowFrom(raw.allowFrom)
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Resolve rine credentials using core's 3-level config-dir fallback
|
|
97
|
-
* ($RINE_CONFIG_DIR > ~/.config/rine > $PWD/.rine). An explicit `cfg.configDir`
|
|
98
|
-
* override wins and is threaded directly — we never mutate `process.env`. Reuses core
|
|
99
|
-
* helpers; no host keychain access, no token writes.
|
|
100
|
-
*/
|
|
101
|
-
function readRineCredentials(cfg) {
|
|
102
|
-
const configDir = cfg.configDir ?? resolveConfigDir();
|
|
103
|
-
const apiUrl = cfg.baseUrl ?? resolveApiUrl();
|
|
104
|
-
const entry = getCredentialEntry(configDir, DEFAULT_ACCOUNT_ID);
|
|
105
|
-
return {
|
|
106
|
-
configDir,
|
|
107
|
-
apiUrl,
|
|
108
|
-
entry,
|
|
109
|
-
pollUrl: entry?.poll_url
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
export { INTERNAL_TOOLS as a, DEFAULT_ACCOUNT_ID as i, resolveRineConfig as n, rinePlugin as r, readRineCredentials as t };
|