@rine-network/mastra 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/AGENTS.md CHANGED
@@ -3,20 +3,21 @@
3
3
  Native [Mastra.ai](https://mastra.ai) tools for the [rine](https://rine.network) network: E2E-encrypted
4
4
  (HPKE 1:1, **MLS** groups RFC 9420, **PQ-hybrid** X25519+ML-KEM-768) agent-to-agent messaging,
5
5
  discovery, and coordination. A thin, typed adapter over the [`@rine-network/sdk`](https://www.npmjs.com/package/@rine-network/sdk)
6
- TS SDK the TS sibling of `langchain-rine` / `crewai-rine`. The SDK owns all crypto/HTTP/config/types.
6
+ TS SDK, which owns all crypto/HTTP/config/types.
7
7
 
8
- `rineToolkit({ agent })` returns a keyed `Record<string, Tool>` (the 11 `rine_*` tools) you spread into
8
+ `rineToolkit({ agent })` returns a keyed `Record<string, Tool>` (the 25 `rine_*` tools) you spread into
9
9
  a Mastra `Agent`'s `tools` map; identity is host-injected via `RequestContext` / toolkit opts, never
10
- chosen by the model, so credentials never enter the model's context. Unlike the Python packages, the TS
11
- SDK **decrypts MLS + PQ-hybrid**, so a Mastra agent can create, read, and post MLS group traffic.
10
+ chosen by the model, so credentials never enter the model's context. The TS SDK **decrypts MLS +
11
+ PQ-hybrid**, so a Mastra agent creates, reads, and posts MLS group traffic.
12
12
  Onboard once out-of-band for E2EE keys (`npx @rine-network/mastra onboard …`, ~30–60 s PoW) — never as a
13
13
  tool. Node `>=22.13.0`, single peer `zod`.
14
14
 
15
- Quick footguns: env creds authenticate but the private keys must be on disk to decrypt/sign;
16
- `rine_reply` is 1:1-only (answer a group with `rine_send` to `#group@org`); detect MLS via
17
- `mls_enabled || mls_group_id !== null || mls_pending`, not the `EncryptionVersion` enum; raise the MCP
18
- tool-call timeout to `>=300000` for `send_and_wait`.
15
+ Quick gotchas: env creds authenticate but the private keys must be on disk to decrypt/sign;
16
+ `rine_reply` is 1:1-only (answer a group with `rine_send` to `#logistics@acme.rine.network`); a group has three
17
+ encryption states, so read them with the SDK's `groupIsMls` / `groupMlsInitInFlight` predicates
18
+ never `mls_enabled`, never the `EncryptionVersion` enum; raise the MCP tool-call timeout to
19
+ `>=300000` for `send_and_wait`.
19
20
 
20
- **Full rules: see [MASTRA.md](./MASTRA.md)** (also at <https://docs.rine.network/mastra.md>) — the 11
21
- tools, the `createTool`/`rineToolkit` and MCP rails, the invariants (I-1/I-2/I-3), every footgun, the
22
- Tier-3 idle-wake bridge, and the onboard step.
21
+ **Full rules: see [MASTRA.md](./MASTRA.md)** (also at <https://docs.rine.network/mastra.md>) — the 25
22
+ tools, the `createTool`/`rineToolkit` and MCP rails, the guarantees the package makes, every gotcha,
23
+ the workflow suspend/resume idle-wake bridge, and the onboard step.
package/MASTRA.md CHANGED
@@ -7,8 +7,7 @@
7
7
  > `inputSchema` → one `AsyncRineClient` method → a human-readable string. The SDK owns all crypto
8
8
  > (HPKE, **MLS**, **PQ-hybrid**), HTTP, config resolution, retries, and types — never reimplement them.
9
9
  >
10
- > The TypeScript sibling of the Python `langchain-rine` / `crewai-rine` packages. `@rine-network/mastra`
11
- > **v0.1.0** · Node `>=22.13.0` · single peer `zod` (`>=3.25.0 || >=4.0.0`) · License EUPL-1.2 ·
10
+ > Node `>=22.13.0` · single peer `zod` (`>=3.25.0 || >=4.0.0`) · License EUPL-1.2 ·
12
11
  > repo `codeberg.org/rine/rine-mastra`.
13
12
 
14
13
  ## Wire it in one shot
@@ -31,7 +30,7 @@ export const rineAgent = new Agent({
31
30
  "end-to-end-encrypted network message. Use rine_discover/rine_inspect to resolve a " +
32
31
  "handle before messaging a stranger; report message and conversation ids back.",
33
32
  model: openai("gpt-4o-mini"),
34
- tools: rineToolkit({ agent: "support" }), // all 11 rine_* tools, keyed by id
33
+ tools: rineToolkit({ agent: "support" }), // all 25 rine_* tools, keyed by id
35
34
  });
36
35
 
37
36
  export const mastra = new Mastra({ agents: { rineAgent } });
@@ -40,7 +39,7 @@ export const mastra = new Mastra({ agents: { rineAgent } });
40
39
  `include` curates the surface (the safe default for a mutating agent is a tight one):
41
40
 
42
41
  ```ts
43
- // "all" (default, 11) | one domain | an array union of domains
42
+ // "all" (default, 25) | one domain | an array union of domains
44
43
  tools: rineToolkit({ agent: "support", include: ["discovery", "messaging"] });
45
44
  ```
46
45
 
@@ -48,17 +47,17 @@ Attach individual factories when you want a hand-picked surface — each
48
47
  `createRine<X>Tool(opts)` is re-exported and returns one `Tool`:
49
48
 
50
49
  ```ts
51
- import { createRineDiscoverTool, createRineSendAndWaitTool, createRineCheckInboxTool } from "@rine-network/mastra";
50
+ import { createRineDiscoverTool, createRineSendAndWaitTool, createRineInboxTool } from "@rine-network/mastra";
52
51
  const tools = {
53
52
  rine_discover: createRineDiscoverTool({ agent: "support" }),
54
53
  rine_send_and_wait: createRineSendAndWaitTool({ agent: "support" }),
55
- rine_check_inbox: createRineCheckInboxTool({ agent: "support" }),
54
+ rine_inbox: createRineInboxTool({ agent: "support" }),
56
55
  };
57
56
  ```
58
57
 
59
58
  > **The tool's lookup name is the `rineToolkit()` object KEY, which equals the tool's `id`**
60
59
  > (`toolName === id`) — NOT some derived label. `rineToolkit()` keys every entry by its `id`, so the
61
- > model calls `rine_send`, `rine_check_inbox`, etc.
60
+ > model calls `rine_send`, `rine_inbox`, etc.
62
61
 
63
62
  ### Mastra Studio / playground quickstart
64
63
 
@@ -74,54 +73,78 @@ Select **Rine Agent** and try *"Find an agent that does translation"* → `rine_
74
73
  MLS group called incident-room"* → `rine_group_create`. **Studio gotcha:** an agent NOT listed in the
75
74
  `new Mastra({ agents: {} })` map never appears in Studio — no error, just silently absent.
76
75
 
77
- ## The 11 tools (ids are stable; the `.describe()` text IS the AI-DX)
76
+ ## The 25 tools (ids are stable; the `.describe()` text is what the model reads)
78
77
 
79
78
  | `id` | What it does |
80
79
  |---|---|
81
- | `rine_send` | Send an end-to-end-encrypted 1:1 (`name@org` / UUID) or group (`#group@org`) message. **Mutating.** Returns the new message id + conversation id. |
80
+ | `rine_send` | Send an end-to-end-encrypted 1:1 (`kofi@acme.rine.network` / UUID) or group (`#logistics@acme.rine.network`) message. **Mutating.** Returns the new message id + conversation id. |
82
81
  | `rine_send_and_wait` | Send to a single agent and block up to `waitSeconds` (1–300) for a reply. **1:1 ONLY.** **Mutating.** |
83
- | `rine_check_inbox` | Fetch + decrypt the newest `status:"new"` messages (1:1 and group), then best-effort `markDelivered` them (poll+ack). |
82
+ | `rine_inbox` | Fetch + decrypt messages from the inbox (1:1 and group) under a `status` filter — `new` (the default), `delivered`, `read` or `all`. Only `new` best-effort `markDelivered`s what it returns (poll+ack); the other three re-read mail already acknowledged and mark nothing. |
84
83
  | `rine_read` | Fetch + decrypt one message by UUID; returns sender, type, decrypted body, signature status. |
85
- | `rine_reply` | Reply to a message by UUID, threading into its conversation. **Mutating. 1:1 only — see footgun 5.** |
84
+ | `rine_reply` | Reply to a message by UUID, threading into its conversation. **Mutating. 1:1 only — see rule 5.** |
85
+ | `rine_thread` | The both-sided, decrypted transcript of one conversation or one group, oldest turn first. Name `group` (its handle, its name, or its UUID) or `conversationId` — exactly one of the two. A group's transcript starts where the reading agent was seated. |
86
86
  | `rine_discover` | Search the public agent directory (q/category/language/verified/limit). Unauthenticated. |
87
87
  | `rine_inspect` | Fetch one agent's full public profile by handle (WebFinger-resolved) or UUID. Unauthenticated. |
88
- | `rine_group_create` | Create a coordination group **MLS-encrypted (RFC 9420) by default** (`enableMls` default true). **Mutating.** |
89
- | `rine_group_invite` | Invite an agent into a group your agent administers (handles or UUIDs). **Mutating.** |
90
- | `rine_group_remove` | Remove a member; group keys rotate for forward secrecy. **Mutating.** |
91
- | `rine_group_inspect` | Show a group's E2EE mode (MLS vs sender-key) + members so you can confirm you can read/post. |
88
+ | `rine_discover_groups` | Search the public group directory by name or topic. Returns handles, enrollment policies and member counts for `public`-visibility groups across every org — never a private group, and never any group's members. Unauthenticated. |
89
+ | `rine_whoami` | Show this agent's own org name and slug, trust tier, and every live agent handle in the org. |
90
+ | `rine_groups` | List the groups your org's agents are seated in, with each group's handle, enrollment policy, encryption mode, member count and `conversation_id`. Org-scoped, and each row's `your agents` clause names by handle which of your org's agents are seated in that group: look for the acting agent's own handle there before posting, because an empty clause means none of them is and a send into that group would be refused. To read what the group has been saying since the reading agent joined, name the group to `rine_thread` — its handle or its id; a row's `conversation_id` works too, and is `null` until someone posts. |
91
+ | `rine_group_create` | Create a coordination group **MLS-encrypted (RFC 9420) by default** (`enableMls` default true). `members` invites a roster as the group is founded: under `closed`, `majority` and `unanimity` that roster mints real invitations — including the agents whose invites would otherwise be nominations — because at founding the creator is the only member. On `open` a roster mints nothing; enrolment there is the join itself. **Mutating.** |
92
+ | `rine_group_invite` | Invite one agent, or several at once, into a group your agent belongs to (handles or UUIDs). On a closed group the SDK adds every invitee to the MLS group in one commit; on a majority or unanimity group each outcome is a nomination the electorate decides, not a seat. **Mutating.** |
93
+ | `rine_group_remove` | Remove a member; they are dropped from the roster and from group fan-out. **Mutating.** |
94
+ | `rine_group_inspect` | Show a group's E2EE mode (MLS vs sender-key) and enrollment policy so you can tell how a group is run before posting to it. It covers the groups this org's agents are seated in, and its `your agents` line names by handle which of them hold a seat in this one. It reports what KIND of group it is; `rine_group_roster` reports who is in it. |
95
+ | `rine_group_roster` | List a group's members with their handles, roles (admin/member), and join dates. Your own org's members are marked `(yours)`, and every other member is still listed. Members only — an invitation or a nomination holds a seat without being one, and `rine_group_requests` is what lists those. |
96
+ | `rine_group_join` | Join a group by its handle or its id: accept a pending invite, or join an open-enrollment group found in the public directory. The handle resolves through your org's seats, this agent's invitations, then the directory; a bare name reaches only a group that has already invited this agent. **Mutating.** |
97
+ | `rine_group_invites` | List the invitations and nominations addressed to your agent, told apart by status. |
98
+ | `rine_group_requests` | List a group's outstanding admissions: the vote queue (`pending`), the unaccepted invitations (`invited`), or both (`live`). Members plus `live` is the whole ratchet tree the seat ceiling counts. |
99
+ | `rine_group_vote` | Approve or deny one pending join request. A request is decided by the members the group had when it was filed, and only by those of them still in it: majority needs more than half of them, unanimity all of them, and an agent who joined afterwards does not vote on it. Denials refuse it on that same electorate — half of them under majority, a single one under unanimity — so both bars fall as members leave. An approve that crosses the group's threshold seats the applicant, and on an MLS group mints their ratchet-tree leaf and Welcome as part of the vote. **Mutating.** |
100
+ | `rine_group_leave` | Leave a group. No Remove commit is posted, so the leaf stays in the tree until a member runs the reclamation pass; this host's key material for the group is retired. **Mutating.** |
101
+ | `rine_group_sync` | Catch this host's MLS state up with a group it fell behind. The expensive rung posts one O(members) commit every member downloads. |
102
+ | `rine_group_reclaim` | Seat every invitee still without a ratchet-tree leaf, then retire the leaves no member and no live invitation accounts for. One Remove commit per leaf, each O(members) and billed to every member. **Mutating.** |
103
+ | `rine_pay` | Pay a received `rine.v1.x402_payment_required` quote under the local deny-by-default spend policy. **Mutating.** |
104
+ | `rine_fulfill` | Payee side: verify + settle a received `rine.v1.x402_payment` through a facilitator and reply with a receipt. **Mutating.** |
92
105
 
93
106
  The ids are **byte-identical across CrewAI, LangChain, and Mastra** — a prompt that names `rine_send`
94
107
  works on any of the three.
95
108
 
96
- ## E2EE — MLS + PQ-hybrid WORK here (the headline, the inverse of the Python packages)
109
+ ## E2EE — MLS and PQ-hybrid
97
110
 
98
111
  The TS SDK decrypts **all** rine encryption modes — `hpke-v1`, `hpke-hybrid-v1` (PQ X25519+ML-KEM-768),
99
- `sender-key-v1`, **and `mls-v1`** — and `rine_group_create` defaults to MLS. So a Mastra agent can
100
- **create, read, AND post MLS group traffic and exchange PQ-hybrid 1:1 messages**. This was proven
101
- **live end-to-end against production** (Phase F, 2026-06-11): a real `mls-v1` group round-trip, a real
102
- `hpke-hybrid-v1` 1:1 round-trip, and a cross-process suspend/resume.
112
+ `sender-key-v1`, **and `mls-v1`** — and `rine_group_create` defaults to MLS. So a Mastra agent
113
+ **creates, reads, and posts MLS group traffic and exchanges PQ-hybrid 1:1 messages**.
103
114
 
104
- `rine_group_inspect` renders **`[OK] MLS group readable/postable`** (not a warning) and
105
- `rine_group_create` reports `MLS E2EE`. There is **no MLS ceiling** in TypeScript — do not write any
106
- "cannot read MLS" / sender-key-fallback-only guidance; that is the Python packages' story, not this one.
115
+ `rine_group_inspect` renders one of four `[OK]` linesnever a warning:
107
116
 
108
- ## Invariants (these hold; rely on them)
117
+ - `[OK] MLS group — end-to-end encrypted (RFC 9420), readable/postable from here.`
118
+ - `[OK] MLS group, initialising — end-to-end encrypted. Sends from here already use MLS.`
119
+ - `[OK] sender-key group — readable/postable from here.`
120
+ - `[OK] sender-key group — readable/postable from here. This group was created to run MLS, but its ratchet tree was never founded, so its messages are sealed with sender keys rather than the MLS it was created for. No verb on this surface founds it: a member has to found the group's MLS state.`
109
121
 
110
- - **I-1 side-effect-free import.** Importing `@rine-network/mastra` builds no client, reads no
122
+ Every one of those is a working group. Sender-key is the broadcast path open-enrollment groups run
123
+ on, and every rine stack reads and posts MLS groups on the shared engine.
124
+
125
+ The fourth line is a closed group created to run MLS whose ratchet tree was never founded
126
+ (`groupMlsNeverFounded(g)`, and `unfoundedGroupNote()` is the sentence). It reads, posts and carries
127
+ messages exactly as the third does; what it has not got is the MLS it was created for. This surface
128
+ names no founding verb because `rine_group_reclaim` refuses here — a member founds the group's MLS
129
+ state — and `rine_group_sync` installs that group's waiting sender keys and warns about the same gap.
130
+
131
+ ## Guarantees
132
+
133
+ - **Import is side-effect-free.** Importing `@rine-network/mastra` builds no client, reads no
111
134
  credential, and opens no socket. `getRineClient` is lazy — the `AsyncRineClient` is constructed the
112
135
  first time a tool's `execute` (or a fired lifecycle callback, or a driver `start()`) actually runs.
113
- - **I-2 — no ciphertext in the LLM context.** Tools return decrypted plaintext only; renderers read
136
+ - **No ciphertext reaches the model context.** Tools return decrypted plaintext only; renderers read
114
137
  only `plaintext` / `decrypt_error` / verification fields, never `encrypted_payload` or any envelope.
115
- `outputSchema` is `z.string()`, and `rine_read` / `rine_check_inbox` add a `toModelOutput` redactor
138
+ `outputSchema` is `z.string()`, and `rine_read` / `rine_inbox` / `rine_thread` add a `toModelOutput` redactor
116
139
  as belt-and-suspenders. The idle-wake `resumeData` is plaintext + signature facts only.
117
- - **I-3 errors become strings.** No tool throws. Every failure comes back as a readable `formatError`
140
+ - **Errors come back as strings.** No tool throws. Every failure comes back as a readable `formatError`
118
141
  string (auth / not-authorized / not-found / rate-limit / validation / timeout / crypto / connection /
119
142
  API). Treat a tool result that starts with `Rine auth failed`, `Not found:`, `Rate-limited`, or
120
143
  `Invalid input:` as an actionable signal, not as message content.
121
144
 
122
- ## Imperatives — the rine footguns, stated as rules
145
+ ## Imperatives — the rules that matter
123
146
 
124
- 1. **Env creds alone authenticate but do NOT give you E2EE keys (state this FIRST).**
147
+ 1. **Env creds alone authenticate but do NOT give you E2EE keys.**
125
148
  `RINE_CLIENT_ID` + `RINE_CLIENT_SECRET` authenticate you, but decrypt/sign need the **private keys
126
149
  on disk** at `configDir/keys/<agent>/{signing.key,encryption.key}` (written by `onboard` /
127
150
  `createAgent` / `rotateKeys`). "Just set two env vars" is only half true unless those keys are
@@ -133,13 +156,16 @@ The TS SDK decrypts **all** rine encryption modes — `hpke-v1`, `hpke-hybrid-v1
133
156
  `encrypted_payload`. If you render inbound plaintext yourself, do it defensively:
134
157
  `typeof plaintext === "string" ? plaintext : plaintext?.text ?? JSON.stringify(plaintext)`.
135
158
 
136
- 3. **MLS + PQ-hybrid are fully supported — but detect MLS the right way.** To self-diagnose whether a
137
- group is MLS, use the capability check `groupIsMls(g)` `mls_enabled || mls_group_id !== null ||
138
- mls_pending`, **NOT `mls_group_id !== null` alone**: `mls_group_id` is `null` at group-create return
139
- and latches asynchronously on the separate MLS-init call, so a default MLS group would otherwise
140
- mis-report as "sender-key". Do **not** rely on the `EncryptionVersion` const-enum to detect MLS/PQ
141
- it is MISSING `mls-v1` and `hpke-hybrid-v1` even though the SDK decrypts them. `rine_group_create`
142
- and `rine_group_inspect` already apply this check; mirror it in any custom code.
159
+ 3. **MLS + PQ-hybrid are fully supported — and a group has THREE encryption states, not two.** Use the
160
+ predicates `@rine-network/sdk` exports, one per state: `groupIsMls(g)` (`mls_group_id` non-null
161
+ the server's MLS latch, and the only field that says a group has MLS state) and
162
+ `groupMlsInitInFlight(g)` (the window between the MLS-init call and that latch, during which sends
163
+ already go out as MLS). A group that matches neither is a sender-key group, which is what
164
+ open-enrollment groups run on. **Never read `mls_enabled`** to answer this: it is an intent flag a
165
+ closed group carries from birth and an open-enrollment group carries while running on sender keys,
166
+ so reading it labels sender-key groups MLS. Do **not** rely on the `EncryptionVersion` const-enum to
167
+ detect MLS/PQ either — it is MISSING `mls-v1` and `hpke-hybrid-v1` even though the SDK decrypts them.
168
+ `rine_group_inspect` already applies these predicates; mirror them in any custom code.
143
169
 
144
170
  4. **Install + call invariants.** Node `>=22.13.0` is required (the `@mastra/core` floor). A **single
145
171
  peer `zod`** — a DUAL zod install breaks Mastra's input validation. `rine_send_and_wait`'s timeout is
@@ -150,7 +176,7 @@ The TS SDK decrypts **all** rine encryption modes — `hpke-v1`, `hpke-hybrid-v1
150
176
  5. **GROUP REPLIES: `rine_reply` is 1:1-ONLY.** `client.reply()` (and the server reply endpoint) routes
151
177
  to "the other party" of a 1:1 conversation and **404s for a group member** who owns neither end of a
152
178
  group message. To answer a group message, post a FRESH message with `rine_send` to the group handle
153
- (`#group@org`) — the SDK re-encrypts it `mls-v1`. 1:1 messages thread fine through `rine_reply`.
179
+ (`#logistics@acme.rine.network`) — the SDK re-encrypts it `mls-v1`. 1:1 messages thread fine through `rine_reply`.
154
180
 
155
181
  6. **ONBOARD ONCE for keys — it is NOT a tool.** A ~30–60 s RSA proof-of-work does not belong in an LLM
156
182
  turn. Run it out-of-band at setup time:
@@ -162,15 +188,17 @@ The TS SDK decrypts **all** rine encryption modes — `hpke-v1`, `hpke-hybrid-v1
162
188
  name verbatim, which is exactly why the derivation (or `--agent-name`) exists. `runOnboard` /
163
189
  `parseOnboardArgs` are exported for scripting, but **never expose onboarding as a `createTool`.**
164
190
 
165
- 7. **Group send is `to="#group@org"` on `rine_send` — there is NO separate group-send tool.** A
191
+ 7. **Group send is `to="#logistics@acme.rine.network"` on `rine_send` — there is NO separate group-send tool.** A
166
192
  `#`-prefixed target routes `rine_send` through the group E2EE path (the SDK auto-picks MLS vs
167
- sender-key from the group's `mls_group_id` latch). `rine_send_and_wait` to a `#` handle returns the
168
- "send_and_wait is 1:1 only; use rine_send for groups." string **without an HTTP call**. Group messages
169
- arrive in `rine_check_inbox` / `rine_read` with its group context shown.
193
+ sender-key with `groupAcceptsMlsSendOnly`, which is true for a latched MLS group **and** for one
194
+ whose init is still in flight a send in that window is an MLS send).
195
+ `rine_send_and_wait` to a `#` handle returns the
196
+ "rine_send_and_wait is 1:1 only; use rine_send for groups." string **without an HTTP call**. Group messages
197
+ arrive in `rine_inbox` / `rine_read` with its group context shown.
170
198
 
171
199
  8. **Never expose admin / destructive SDK operations as tools.** `onboard`, `eraseOrg`, `exportOrg`,
172
200
  and the streaming/`defineAgent` SSE loop are SDK-level operations, not agent tools. Keep them out of
173
- the toolkit; the 11 tools are the whole agent-facing surface.
201
+ the toolkit; the 25 tools are the whole agent-facing surface.
174
202
 
175
203
  ## Identity injection — host-set, never model-chosen
176
204
 
@@ -187,11 +215,11 @@ requestContext.set(RINE_ACTING_AGENT, "support");
187
215
  requestContext.set(RINE_CONFIG_DIR, "/srv/rine/.rine");
188
216
  ```
189
217
 
190
- Credentials NEVER enter the model-visible `inputSchema` (Mastra's own security rule == I-2). All tools
218
+ Credentials NEVER enter the model-visible `inputSchema` (Mastra's own security rule). All tools
191
219
  from one `rineToolkit(...)` call share ONE lazily-built `AsyncRineClient` (and its warm OAuth token
192
220
  cache); per-acting-agent variants derive cheaply via `client.withAgent(...)`.
193
221
 
194
- ## Lifecycle bridge (native beats MCP)
222
+ ## Lifecycle bridge
195
223
 
196
224
  `rineLifecycle({ to, on })` returns the `{ onFinish, onError, onStepFinish }` callbacks Mastra's
197
225
  `agent.stream(...)` / `agent.generate(...)` accept — a hook an out-of-process MCP server physically
@@ -211,7 +239,7 @@ text / tool name / error message (truncated at 500 chars) — never ciphertext.
211
239
 
212
240
  A Mastra agent can receive, not just send.
213
241
 
214
- - **Poll and ack on turn:** call `rine_check_inbox` inside the agent loop. It fetches
242
+ - **Poll and ack on turn:** call `rine_inbox` inside the agent loop. It fetches
215
243
  `status:"new"`, decrypts, and best-effort `markDelivered`s the decryptable ids so the next check
216
244
  returns only newer messages. On ack failure it warns and notes the messages "may reappear" — but still
217
245
  returns the reads. Zero new infra.
@@ -222,10 +250,9 @@ A Mastra agent can receive, not just send.
222
250
  ### Idle wake for Mastra workflows
223
251
 
224
252
  A `RineThreadResumer` wakes a **suspended, durably-checkpointed** Mastra workflow run when the peer's
225
- reply lands — across **process AND org boundaries**. Positioning: Mastra's native suspend/resume is
226
- in-process / shared-storage / same-deployment; rine is the *cross-process / cross-org* complement —
227
- "handoffs that survive process and org boundaries." It turns a Mastra HITL pause into an
228
- agent-in-the-loop pause; it is complementary to Mastra's in-process workflows, not competing.
253
+ reply lands — across **process AND org boundaries**. Mastra's native suspend/resume is in-process,
254
+ shared-storage and same-deployment; `RineThreadResumer` resumes across process and org boundaries.
255
+ It turns a Mastra HITL pause into an agent-in-the-loop pause.
229
256
 
230
257
  Wire it on a long-lived host: a workflow built on **shared** snapshot storage, a durable thread-map,
231
258
  the resumer, and a driver.
@@ -264,22 +291,22 @@ The pitfalls (rules):
264
291
  - **A step can suspend only once per execution.** Avoid parallel suspends in one superstep.
265
292
  - **Lossless-by-default.** Every skip path (no mapping, not suspended, decrypt-failed, no conversation,
266
293
  resume threw) leaves the message in the rine inbox — the resumer NEVER acks. Run it ALONGSIDE normal
267
- inbox handling (drain stragglers with `rine_check_inbox`); it is not a full inbox drain. The thread-map
268
- is unbounded in v0.1 — prune completed `(handle, conversation)` entries with `threadMap.delete(...)`.
294
+ inbox handling (drain stragglers with `rine_inbox`); it is not a full inbox drain. The thread-map
295
+ is unbounded — prune completed `(handle, conversation)` entries with `threadMap.delete(...)`.
269
296
  - **Reply-timeout: bring your own deadline.** There is no scheduler. Track each parked run's deadline
270
297
  in your own state and resume it yourself when it fires; a run you never wake waits forever.
271
298
  - **Webhook alternative:** `makeWebhookHandler({ resumer })` returns a `Callable` you mount on your own
272
299
  HTTP route on a rine outbound-webhook delivery. **Verify the rine outbound-webhook signature first**,
273
300
  decrypt the payload into a `DecryptedMessage` via the SDK, then pass it to the handler — never resume
274
301
  from an unauthenticated POST body. It dispatches to `resumer.handleInbound(msg)` exactly like one poll.
275
- - **Serverless caveat (shout it):** a poll loop and Mastra's built-in scheduler **silently never fire**
302
+ - **Serverless caveat:** a poll loop and Mastra's built-in scheduler **silently never fire**
276
303
  on Vercel/Netlify/Lambda/CF Workers — the process dies between requests. Use `@mastra/inngest` there.
277
304
 
278
305
  ## MCP rail (zero-new-code quickstart, documented)
279
306
 
280
307
  For a "works today" path, point `@mastra/mcp`'s `MCPClient` at the existing `@rine-network/mcp` stdio
281
- server. The native package is the product (typed Zod tools, no MCP-timeout wart, the idle-wake in-process
282
- resume MCP cannot do) — the MCP rail is the warm-up.
308
+ server. The native package provides typed Zod tools, no MCP tool-call timeout to raise, and the
309
+ in-process idle-wake resume; the MCP rail needs no new code.
283
310
 
284
311
  ```ts
285
312
  import { MCPClient } from "@mastra/mcp";
@@ -305,7 +332,7 @@ native package types all of this.
305
332
  - Put a **rich `.describe()` on EVERY Zod field** — field descriptions are the #1 lever on tool-call
306
333
  accuracy.
307
334
  - Each tool `description` is a **complete sentence stating the action AND the return shape**.
308
- - **NEVER put identity or credentials in `inputSchema`** (Mastra's own security rule == I-2) — they are
335
+ - **NEVER put identity or credentials in `inputSchema`** (Mastra's own security rule) — they are
309
336
  host-injected via `RequestContext` / `rineToolkit()` opts.
310
337
  - Keep tool **ids unique and identical across CrewAI / LangChain / Mastra** so prompts port between
311
338
  stacks unchanged.
@@ -313,9 +340,9 @@ native package types all of this.
313
340
  ## Do NOT
314
341
 
315
342
  - Promise an "awesome-mastra" list, a Mastra registry, or a "Featured" listing — **none exists**.
316
- - Invent SDK methods/fields. The whole surface is the 11 tools + `rineToolkit` + `rineLifecycle` +
343
+ - Invent SDK methods/fields. The whole surface is the 25 tools + `rineToolkit` + `rineLifecycle` +
317
344
  the idle-wake apparatus + `runOnboard`/`parseOnboardArgs` + `getRineClient` + `formatError`.
318
- - Write any "MLS ceiling" / "cannot read MLS" / sender-key-fallback-only languageMLS + PQ work here.
345
+ - Describe any rine surface as unable to read or post MLS groupsevery rine stack does both.
319
346
 
320
347
  ## For AI agents
321
348
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ npm install @rine-network/mastra @mastra/core zod
13
13
  Requires Node `>=22.13.0` (the `@mastra/core` floor). Onboard a rine identity once (org + agent, ~30–60 s RSA proof-of-work):
14
14
 
15
15
  ```bash
16
- npx @rine-network/mastra onboard --email you@example.com --slug my-org --name "My Org"
16
+ npx @rine-network/mastra onboard --email you@yourdomain.com --slug my-org --name "My Org"
17
17
  ```
18
18
 
19
19
  ## Quickstart
@@ -28,27 +28,38 @@ export const rineAgent = new Agent({
28
28
  name: "Rine Agent",
29
29
  instructions: "You message and coordinate with other agents over rine.",
30
30
  model: openai("gpt-4o-mini"),
31
- tools: rineToolkit({ agent: "support" }), // all 14 rine_* tools, keyed by id
31
+ tools: rineToolkit({ agent: "support" }), // all 25 rine_* tools, keyed by id
32
32
  });
33
33
  ```
34
34
 
35
35
  `rineToolkit()` returns a keyed `Record<string, Tool>` — spread it into a Mastra `Agent`'s `tools` map. The acting identity (`agent`, `configDir`, `apiUrl`) is host-injected, never chosen by the model, so credentials never enter the model's context.
36
36
 
37
- ## The 14 tools
37
+ ## The 25 tools
38
38
 
39
39
  | Tool | What it does |
40
40
  |---|---|
41
- | `rine_send` | Send an encrypted 1:1 or group (`#group@org`) message. |
41
+ | `rine_send` | Send an encrypted 1:1 or group message. A group is named `#logistics@acme.rine.network`, by its own name, or by UUID. |
42
42
  | `rine_send_and_wait` | Send and block for a reply (1:1, up to 300 s). |
43
- | `rine_check_inbox` | Poll + ack the newest decrypted unread messages. |
43
+ | `rine_inbox` | Fetch decrypted messages under a `status` filter — `new` (default), `delivered`, `read` or `all`. Only `new` acks what it returns. |
44
44
  | `rine_read` | Read one message by id. |
45
45
  | `rine_reply` | Reply into a message's conversation. |
46
- | `rine_thread` | Decrypted both-sided transcript of a conversation. |
46
+ | `rine_thread` | Decrypted both-sided transcript of a conversation or a group. Name `group` (handle, name or UUID) or `conversationId` — exactly one of the two. A group's transcript starts where the reading agent was seated. |
47
47
  | `rine_discover` | Search the public agent directory. |
48
48
  | `rine_inspect` | Inspect one agent's profile. |
49
- | `rine_group_create` | Create a group **MLS-encrypted by default**. |
50
- | `rine_group_invite` / `rine_group_remove` | Manage group membership. |
51
- | `rine_group_inspect` | Inspect a group's E2EE mode + members. |
49
+ | `rine_whoami` | This agent's own org, trust tier, and live handles. |
50
+ | `rine_discover_groups` | Search the public group directory across every org. Publicly listed groups only, never their members. |
51
+ | `rine_groups` | List the groups your org's agents are seated in — handle, enrollment policy, encryption mode, member count, and the group's `conversation_id`. The list is org-scoped, and each row's `your agents` clause names by handle which of your org's agents are seated in that group: look for the acting agent's own handle there before posting, because an empty clause means none of them is and a send into that group would be refused. To read what has been said in the group since the reading agent joined, name the group to `rine_thread` — its handle or its id; a row's `conversation_id` works too, and a group nobody has posted in yet has none. |
52
+ | `rine_group_create` | Create a group — **post-quantum MLS by default**. `visibility` is required; `members` invites a roster as the group is founded, and a founding roster mints real invitations under every enrollment policy because at founding the creator is the only member. `voteDurationHours` (1-72, default 72) sets how long a join-request vote stays open on a majority/unanimity group. `description` is server-visible house rules, NOT end-to-end encrypted. |
53
+ | `rine_group_invite` / `rine_group_remove` | Invite one agent or several at once — on a closed group the SDK adds every invitee to the MLS group in one commit, and on a majority or unanimity group each outcome is a nomination the electorate decides, not a seat; remove a member, which on an MLS group posts a Remove commit that takes their ratchet-tree leaf with it. |
54
+ | `rine_group_inspect` | Inspect a group's E2EE mode and enrollment policy — what kind of group it is. Its `your agents` line names by handle which of your org's agents are seated in it. |
55
+ | `rine_group_roster` | List a group's members with their roles and join dates — who is in it. Your own org's members are marked `(yours)`; every other member is still listed. |
56
+ | `rine_group_join` | Join a group by its handle or its id — accept a pending invite, or request to join a group found in the public directory. A bare name reaches only a group that has already invited this agent. **Mutating.** |
57
+ | `rine_group_invites` | List the invitations and nominations addressed to your agent, told apart by status. |
58
+ | `rine_group_requests` | List a group's outstanding admissions: the vote queue, the unaccepted invitations, or both. Members plus `live` is the whole ratchet tree the seat ceiling counts. |
59
+ | `rine_group_vote` | Approve or deny one pending join request. A request is decided by the members the group had when it was filed, and only by those of them still in it: majority needs more than half of them, unanimity all of them, and an agent who joined afterwards does not vote on it. Denials refuse it on that same electorate — half of them under majority, a single one under unanimity — so both bars fall as members leave. An approve that crosses the group's threshold seats the applicant, and on an MLS group mints their ratchet-tree leaf and Welcome as part of the vote. **Mutating.** |
60
+ | `rine_group_leave` | Leave a group. No Remove commit is posted, so the leaf stays in the tree until a member runs the reclamation pass; this host's key material for the group is retired. **Mutating.** |
61
+ | `rine_group_sync` | Catch this host's MLS state up with a group it fell behind. The expensive rung posts one O(members) commit every member downloads. |
62
+ | `rine_group_reclaim` | Seat every invitee still without a ratchet-tree leaf, then retire the leaves no member and no live invitation accounts for. One Remove commit per leaf, each O(members) and billed to every member. **Mutating.** |
52
63
  | `rine_pay` | Pay a received `rine.v1.x402_payment_required` quote under the local spend policy. **Mutating.** |
53
64
  | `rine_fulfill` | Payee side: verify + settle a received `rine.v1.x402_payment` through a facilitator and reply with a receipt. **Mutating.** |
54
65
 
package/dist/_zod.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Host-zod re-export + the shared string output schema.
3
3
  *
4
- * Every tool's `outputSchema` is `z.string()` (D-output) — a tool returns clean
4
+ * Every tool's `outputSchema` is `z.string()` — a tool returns clean
5
5
  * text to the model, so ciphertext can never ride a struct field and benign
6
- * "no reply" strings never trip v1 output-validation (PHASE0_CONTRACT P0-2).
6
+ * "no reply" strings never trip v1 output-validation.
7
7
  * This is authored with the HOST `zod` (the single hoisted instance Mastra
8
8
  * validates against), NOT the SDK's re-exported `z`. Centralizing the one shared
9
9
  * instance here keeps `import { z } from "zod"` out of every tool module while
package/dist/client.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Lazy `AsyncRineClient` construction (invariant I-1: side-effect-free import).
2
+ * Lazy `AsyncRineClient` construction, so that importing this package stays
3
+ * side-effect-free.
3
4
  *
4
5
  * No client is built at module load or in any `createTool` factory body — the
5
6
  * SDK client is created the first time a tool's `execute` actually fires, then
6
7
  * memoized per distinct option set so all tools from one `rineToolkit(...)`
7
- * call share ONE client (AC-11; the client holds no sockets, so sharing is
8
+ * call share ONE client (the client holds no sockets, so sharing is
8
9
  * cheap and keeps the OAuth token cache warm).
9
10
  *
10
- * Two footguns the SDK leaves to the caller are closed here (D-config-resolve):
11
+ * Two footguns the SDK leaves to the caller are closed here:
11
12
  * - `configDir` defaults to `""` in the SDK constructor → it would silently
12
13
  * write/read keys against `process.cwd()`. We resolve it explicitly via
13
14
  * `resolveConfigDir()` unless the caller passed one.
@@ -29,7 +30,8 @@ export interface RineClientOpts {
29
30
  * already scoped to `opts.agent` when present.
30
31
  *
31
32
  * Call this ONLY from inside an `execute` / a fired callback / a driver start —
32
- * never at module top level or in a factory body (I-1).
33
+ * never at module top level or in a factory body. Importing this package must
34
+ * open no socket, read no credential and construct no client.
33
35
  */
34
36
  export declare function getRineClient(opts?: RineClientOpts): AsyncRineClient;
35
37
  /**
package/dist/context.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Identity injection via Mastra's `RequestContext` (P0-3, BLOCKING — verified
2
+ * Identity injection via Mastra's `RequestContext` (BLOCKING — verified
3
3
  * in-rig). The acting agent + config dir are read from `ctx.requestContext`
4
- * inside a tool's `execute`, NEVER from the model-visible `inputSchema` (I-2 /
5
- * Mastra's own security rule). Keys are `rine-`-namespaced to avoid collision
4
+ * inside a tool's `execute`, NEVER from the model-visible `inputSchema` the
5
+ * host injects the identity, the model never chooses it (also Mastra's own
6
+ * security rule). Keys are `rine-`-namespaced to avoid collision
6
7
  * with Mastra's reserved `mastra__*` keys.
7
8
  *
8
9
  * Import the `RequestContext` class itself from `@mastra/core/request-context`
@@ -1,11 +1,17 @@
1
1
  /**
2
- * The 2 discovery tools: `rine_discover`, `rine_inspect` (SPEC §6 discovery
3
- * table). Both are unauthenticated directory reads. `discover` exposes only the
2
+ * The 4 discovery tools. `rine_discover` and `rine_inspect` are unauthenticated
3
+ * directory reads; `rine_whoami` reads this agent's OWN identity, so it is the
4
+ * one authenticated verb here. `discover` exposes only the
4
5
  * fields the TS `DiscoveryFilters` supports — q/category/language/verified/limit
5
- * (the Python tag/jurisdiction/pricingModel fields are dropped, D-discover-fields).
6
+ * (the Python tag/jurisdiction/pricingModel fields are dropped).
7
+ *
8
+ * `rine_discover_groups` is the directory's group half and lives with the other
9
+ * group readers in `groups-list.ts`; only `tools.ts` files it under discovery.
6
10
  */
7
11
  import { type RineToolOpts } from "./tool.js";
8
12
  /** `rine_discover` — search the public agent directory. */
9
13
  export declare function createRineDiscoverTool(opts?: RineToolOpts): import("@mastra/core/tools").Tool<any, any, any, any, import("@mastra/core/tools").ToolExecutionContext<any, any, unknown>, "rine_discover", unknown>;
10
14
  /** `rine_inspect` — fetch one agent's full public profile. */
11
15
  export declare function createRineInspectTool(opts?: RineToolOpts): import("@mastra/core/tools").Tool<any, any, any, any, import("@mastra/core/tools").ToolExecutionContext<any, any, unknown>, "rine_inspect", unknown>;
16
+ /** `rine_whoami` — this agent's own org, trust tier, and live handles. */
17
+ export declare function createRineWhoamiTool(opts?: RineToolOpts): import("@mastra/core/tools").Tool<any, any, any, any, import("@mastra/core/tools").ToolExecutionContext<any, any, unknown>, "rine_whoami", unknown>;
package/dist/drivers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tier-3 wake drivers (D-tier3-driver). Two ways to feed inbound rine messages
2
+ * Tier-3 wake drivers. Two ways to feed inbound rine messages
3
3
  * into a {@link RineThreadResumer}:
4
4
  *
5
5
  * - {@link PollDriver} (default) — a long-lived host loop over the SDK's
@@ -13,7 +13,7 @@
13
13
  * poll loop **silently never fire** on Vercel/Netlify/Lambda/CF Workers (the
14
14
  * process dies between requests) — `@mastra/inngest` is required there.
15
15
  *
16
- * I-1: NO `AsyncRineClient` and NO SSE loop are built at construction. The
16
+ * NO `AsyncRineClient` and NO SSE loop are built at construction. The
17
17
  * `PollDriver` builds its client + `defineAgent` only on `start()`; the webhook
18
18
  * handler builds nothing until it is actually called.
19
19
  */
@@ -41,7 +41,7 @@ export declare class PollDriver {
41
41
  constructor(opts: PollDriverOptions);
42
42
  /**
43
43
  * Start the SSE delivery loop. Lazily builds the `AsyncRineClient` + the
44
- * `defineAgent` actor (I-1 — nothing networked until now). Idempotent: a
44
+ * `defineAgent` actor — nothing is networked until now. Idempotent: a
45
45
  * second call is a no-op while a loop is running.
46
46
  */
47
47
  start(): Promise<void>;
@@ -63,7 +63,8 @@ export interface WebhookHandlerOptions {
63
63
  * The opt-in webhook wake driver. Returns a `Callable` the user invokes from
64
64
  * their own HTTP route on a rine outbound webhook delivery; it dispatches the
65
65
  * decrypted message to {@link RineThreadResumer.handleInbound} and reports
66
- * whether a run was resumed. I-1: builds nothing until called.
66
+ * whether a run was resumed. It builds nothing — no client, no loop — until
67
+ * it is actually called.
67
68
  *
68
69
  * The caller is responsible for decrypting the webhook payload into a
69
70
  * `DecryptedMessage` (e.g. via the SDK) before invoking the handler — keeping
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `formatError(err)` — invariant I-3: turn any thrown SDK error into a readable
2
+ * `formatError(err)` — turns any thrown SDK error into a readable
3
3
  * string for the LLM, never a stack trace. Tools wrap their one `await client.*`
4
4
  * call in `try/catch → formatError` and RESOLVE (never reject) for mapped errors.
5
5
  *
@@ -17,10 +17,17 @@
17
17
  */
18
18
  /**
19
19
  * True when `err` is the plain `Error` `sendAndWait` throws for a group handle.
20
- * The `send_and_wait` tool checks this BEFORE `formatError` and returns the
21
- * "1:1 only" guidance; it is not a typed SDK error class.
20
+ * The `rine_send_and_wait` tool checks this BEFORE `formatError` and returns
21
+ * the "1:1 only" guidance; it is not a typed SDK error class.
22
22
  */
23
23
  export declare function isGroupUnsupportedOnWait(err: unknown): boolean;
24
- /** The fixed reply for the group-on-`sendAndWait` case. */
25
- export declare const GROUP_ON_WAIT_MESSAGE = "send_and_wait is 1:1 only; use rine_send for groups.";
24
+ /**
25
+ * The fixed reply for the group-on-`sendAndWait` case.
26
+ *
27
+ * 🔴 It names `rine_send_and_wait`, the id this surface actually registers
28
+ * (`./messaging.ts`). A model reads a refusal literally and calls the verb the
29
+ * sentence names, so a bare `send_and_wait` here spends the turn on a
30
+ * tool-not-found.
31
+ */
32
+ export declare const GROUP_ON_WAIT_MESSAGE = "rine_send_and_wait is 1:1 only; use rine_send for groups.";
26
33
  export declare function formatError(err: unknown): string;