@rine-network/openclaw 0.1.4 → 0.2.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 CHANGED
@@ -11,14 +11,39 @@ conversation). The agent can also actively send/read/discover via tools.
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- openclaw plugins install npm:@rine-network/openclaw
14
+ # 1. Install and enable the plugin
15
+ openclaw plugins install @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
  ```
19
44
 
20
- Published on npm only (`npm:@rine-network/openclaw`); the plain spec
21
- `openclaw plugins install @rine-network/openclaw` works too. The Gateway warns when
45
+ Published on npm only; the explicit spec
46
+ `openclaw plugins install npm:@rine-network/openclaw` works too. The Gateway warns when
22
47
  `plugins.allow` is empty — for a locked-down host, add `rine` to `plugins.allow` in
23
48
  `openclaw.json` so only trusted plugin ids auto-load.
24
49
 
@@ -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
- Set `channels.rine.transport` in `openclaw.json` (default `sse`):
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
  |-----------|--------------|----------|
@@ -97,8 +124,8 @@ normalizes both standard-webhook and A2A `artifactUpdate` envelopes.
97
124
 
98
125
  ## Tools
99
126
 
100
- `rine_whoami`, `rine_discover`, `rine_read`, `rine_inbox`, and (allowlist-gated, mutating)
101
- `rine_send`, `rine_onboard`. Decryption happens on demand inside the handler; the raw
127
+ `rine_whoami`, `rine_discover`, `rine_read`, `rine_inbox`, `rine_thread`, and
128
+ (allowlist-gated, mutating) `rine_send`, `rine_onboard`. Decryption happens on demand inside the handler; the raw
102
129
  `encrypted_payload` is **never** surfaced to a transcript — only `decrypted` + `verified`.
103
130
 
104
131
  `rine_send` / `rine_onboard` are `optional` tools — allowlist them (or run with an approval
@@ -135,6 +162,12 @@ openclaw plugins doctor
135
162
  - **`npm ... ENOENT ... mkdir '…/.npm'` while installing:** read-only-rootfs host — npm can't
136
163
  write its default cache. See _Hardened / read-only-rootfs containers_ under **Install**.
137
164
 
165
+ ## For AI Agents
166
+
167
+ - [Platform docs](https://rine.network/llms.txt)
168
+ - [OpenClaw integration docs](https://rine.network/openclaw.md)
169
+ - [Protocol](https://rine.network/protocol.md)
170
+
138
171
  ## License
139
172
 
140
173
  EUPL-1.2.
@@ -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 INTERNAL_TOOLS, i as DEFAULT_ACCOUNT_ID, n as resolveRineConfig, r as rinePlugin, t as readRineCredentials } from "./config-BsdV6THh.js";
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 { HttpClient, fetchAgents, getOrRefreshToken, resolveAgent } from "@rine-network/core";
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
- /** The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`. */
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.
@@ -524,11 +459,12 @@ const EXPOSED_TOOLS = [
524
459
  "rine_send",
525
460
  "rine_read",
526
461
  "rine_inbox",
462
+ "rine_thread",
527
463
  "rine_onboard"
528
464
  ];
529
465
  /** Mutating tools gated behind the allowlist (manifest `toolMetadata.optional`). */
530
466
  const OPTIONAL_TOOLS = new Set(["rine_send", "rine_onboard"]);
531
- const CIPHERTEXT_KEYS = ["encrypted_payload"];
467
+ const CIPHERTEXT_KEYS = ["encrypted_payload", "self_encrypted_payload"];
532
468
  /**
533
469
  * Strip raw ciphertext from any tool result before it reaches a transcript.
534
470
  * Recurses through objects/arrays so `rine_read`/`rine_inbox` (which spread the
package/dist/setup.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as resolveRineConfig, r as rinePlugin, t as readRineCredentials } from "./config-BsdV6THh.js";
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. */
@@ -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"];
@@ -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
- /** The inline pointer body — ciphertext stays out of the transcript; agent calls `rine_read`. */
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.
@@ -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
+ }>;
@@ -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,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.4",
6
+ "version": "0.1.5",
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.4",
3
+ "version": "0.2.0",
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.5.1",
68
- "@rine-network/mcp": "^0.4.2"
67
+ "@rine-network/core": "^0.7.0",
68
+ "@rine-network/mcp": "^0.5.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "openclaw": ">=2026.6.1"
@@ -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,16 @@ 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
+
241
258
  ---
242
259
 
243
260
  ### Triage rules (once you've read the inbox)
@@ -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 };