@rine-network/eve 0.3.0 → 0.5.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.
Files changed (39) hide show
  1. package/README.md +12 -6
  2. package/dist/{channel-WlN3x1wP.js → channel-B81UiHyr.js} +12 -9
  3. package/dist/channel-core.d.ts +9 -6
  4. package/dist/channel.d.ts +1 -1
  5. package/dist/channel.js +1 -1
  6. package/dist/{client-DsG2xtKs.js → client-CXJATA-m.js} +22 -2
  7. package/dist/client.d.ts +18 -0
  8. package/dist/format-groups-list.d.ts +140 -0
  9. package/dist/format-groups.d.ts +81 -0
  10. package/dist/format.d.ts +51 -11
  11. package/dist/index.js +7 -7
  12. package/dist/onboard.js +2 -2
  13. package/dist/{registry-Bn4EqPcp.js → registry-DsU13KY3.js} +47 -2
  14. package/dist/relay.js +2 -2
  15. package/dist/scaffold-DQ2CA1kD.js +239 -0
  16. package/dist/scaffold.js +1 -1
  17. package/dist/schemas-groups-list.d.ts +29 -0
  18. package/dist/schemas-groups.d.ts +83 -9
  19. package/dist/schemas.d.ts +26 -6
  20. package/dist/skill-content.d.ts +8 -2
  21. package/dist/tool-DeOeMNlK.js +618 -0
  22. package/dist/tool.d.ts +2 -2
  23. package/dist/tools/discovery.d.ts +8 -2
  24. package/dist/tools/groups-admin.d.ts +25 -0
  25. package/dist/tools/groups-admission.d.ts +56 -0
  26. package/dist/tools/groups-list.d.ts +21 -0
  27. package/dist/tools/groups-resolve.d.ts +80 -0
  28. package/dist/tools/groups.d.ts +18 -14
  29. package/dist/tools/index.d.ts +5 -3
  30. package/dist/tools/index.js +3 -3
  31. package/dist/tools/messaging.d.ts +13 -11
  32. package/dist/tools-CQDZG-qN.js +1149 -0
  33. package/dist/types.d.ts +2 -1
  34. package/dist/webhook.d.ts +8 -1
  35. package/dist/webhook.js +33 -13
  36. package/package.json +3 -3
  37. package/dist/scaffold-3cEUy3jD.js +0 -147
  38. package/dist/tool-xlLdY8kn.js +0 -276
  39. package/dist/tools-BhkhV3Mv.js +0 -595
@@ -0,0 +1,618 @@
1
+ import { n as getRineClient, t as actingAgent } from "./client-CXJATA-m.js";
2
+ import { APIConnectionError, AuthenticationError, AuthorizationError, ConfigError, CryptoError, GROUP_ROSTER_LEAD, NO_GROUPS_IN_ORG, NotFoundError, RateLimitError, RineApiError, RineTimeoutError, SchemaValidationError, ValidationError, groupIsMls, groupMlsInitInFlight, groupMlsNeverFounded, senderKeyExplainer, senderKeyExplanation, unfoundedGroupNote } from "@rine-network/sdk";
3
+ import { resolveApiUrl } from "@rine-network/core";
4
+ import { always, never, once } from "eve/tools/approval";
5
+ import { ZodError } from "zod";
6
+ /**
7
+ * What a group renders in place of an agent list when this org holds no seat.
8
+ *
9
+ * The group list is ORG-scoped — it reports every group any agent of this org
10
+ * is in — so a row on it was never proof that the agent this host is acting as
11
+ * may post there. The empty list is that proof's absence stated plainly: no
12
+ * agent of this org is seated, and a send into the group would be refused.
13
+ *
14
+ * Never omitted, for the reason `NO_CONVERSATION` gives above: a clause a model
15
+ * sees on some rows and not others reads as a rendering fault rather than as a
16
+ * fact about the group. Restated per package because there is no runtime module
17
+ * all seven rendering surfaces import — though this package and its twin do
18
+ * share one (`@rine-network/sdk`), so the day either reworded its row would be
19
+ * the day the sentence belongs there instead.
20
+ *
21
+ * Pinned meanwhile: `tests/test_client-server-contract_group0.py` compares the
22
+ * four declared copies against `rine.group_words.NO_OWN_AGENTS`, which the
23
+ * three Python surfaces import, so a reword in one package fails there rather
24
+ * than shipping two answers to one question.
25
+ */
26
+ const NO_OWN_AGENTS = "(none — no agent of your org is seated in this group)";
27
+ /**
28
+ * The seated-agents clause, from the one place both renderers read it.
29
+ *
30
+ * `format.ts`'s `renderGroup` calls this too, so the row and the single-group
31
+ * view cannot tell a model two different things about the same group — the same
32
+ * reason `NO_CONVERSATION` is imported there rather than retyped.
33
+ *
34
+ * Each id renders as its HANDLE. The three cases, and why a name is never one
35
+ * of them, are stated once on `AgentHandleMap` in the SDK
36
+ * (`rine-ts-sdk/src/resources/agent-handles.ts`); this renderer is one of the
37
+ * four that follow them. In short: a known id prints its handle, an unknown one
38
+ * prints itself, an empty list prints {@link NO_OWN_AGENTS}. A model handed a
39
+ * UUID cannot tell which of its agents it names — that is the whole reason
40
+ * handle resolution exists here — but a UUID is still addressable, so an
41
+ * unresolved id is degraded, not lost.
42
+ *
43
+ * `handles` is omitted whenever the org's agent list could not be read; that is
44
+ * case 2 for every id, and it is why nothing here throws.
45
+ *
46
+ * `undefined` is accepted on purpose. `GroupReadSchema` defaults the field to
47
+ * `[]`, so a parsed row always has one — but a row that never went through that
48
+ * parse (a hand-built object, a fake) has nothing, and a renderer whose job is
49
+ * to state an absence must not throw on one.
50
+ */
51
+ function renderOwnAgents(ids, handles) {
52
+ if (!ids || ids.length === 0) return NO_OWN_AGENTS;
53
+ return ids.map((id) => handles?.get(id.toLowerCase()) ?? id).join(", ");
54
+ }
55
+ /**
56
+ * How this package names a group's regime in one field, for a LIST ROW.
57
+ *
58
+ * One definition for the row, and for the row only. It is NOT what the create
59
+ * confirmation head asks: what a freshly created group runs is decided from the
60
+ * create RESULT, by `createdGroupMls` in `@rine-network/core`, because the row
61
+ * alone cannot tell a founded group from an unfounded one. `renderGroup`'s
62
+ * `[OK]` line re-derives its first clause from the predicates directly and has
63
+ * never called this either. So a reword here reaches this row and nothing else,
64
+ * and a reword in either of those two does not reach this row.
65
+ *
66
+ * The init window is reported as MLS because sends into it already are MLS, and
67
+ * a field that said "sender-key" there would state the opposite of what is
68
+ * happening. The window's own distinction is `rine_group_inspect`'s to make.
69
+ *
70
+ * A group created to run MLS whose ratchet tree was never founded really does
71
+ * carry sender-key traffic, so it names sender keys — and says the rest of the
72
+ * answer, because that row is otherwise field-for-field an open group's and
73
+ * nothing else on the page can tell the two apart. The parenthesis carries NO
74
+ * comma: the row below is comma-separated, which is the same reason its last
75
+ * clause opens with `; `. The whole sentence lives in `rine_group_inspect`; a
76
+ * list row is not the place for it.
77
+ */
78
+ function groupModeLabel(g) {
79
+ if (groupIsMls(g) || groupMlsInitInFlight(g)) return "MLS";
80
+ if (groupMlsNeverFounded(g)) return "sender-key (MLS never founded)";
81
+ return "sender-key";
82
+ }
83
+ /**
84
+ * One row of the caller's own groups.
85
+ *
86
+ * The row carries `conversation_id` because this is the only place a model can
87
+ * learn it without having already sent into the group or been handed a raw
88
+ * message row: list the groups, take the id off the row, hand it to
89
+ * `rine_thread`. The key is spelled as the wire field rather than as prose so
90
+ * that the string the model reads and the argument it must fill match.
91
+ *
92
+ * It also names which of this org's agents hold a seat here. `member_count` is
93
+ * the whole roster across every org; this clause is the caller's own share of
94
+ * it, and it is the only thing on an org-scoped row that says whether this org
95
+ * can act in the group at all.
96
+ *
97
+ * That clause closes the row with `; ` rather than `, ` because its VALUE is a
98
+ * comma-joined list. Ending a comma-separated row with a comma-containing field
99
+ * makes the row's field count depend on how many seats the org holds, so a
100
+ * reader that split on `, ` would mis-read multi-seat rows only — and read
101
+ * single-seat ones correctly, which is how that would ship.
102
+ */
103
+ function renderGroupRow(g, handles) {
104
+ const mode = groupModeLabel(g);
105
+ const conversation = g.conversation_id ?? "(none — nothing has been said in this group yet)";
106
+ const yours = renderOwnAgents(g.member_agent_ids, handles);
107
+ return `${g.handle} (id ${g.id}) — ${mode}, enrollment ${g.enrollment_policy}, ${g.member_count} member(s), conversation_id=${conversation}; your agents: ${yours}`;
108
+ }
109
+ /**
110
+ * A numbered list of the groups this org's agents are seated in, or the
111
+ * empty-state line.
112
+ *
113
+ * The list is org-scoped, and each row's `your agents` clause names which of
114
+ * this org's agents are seated in that group — an empty clause means none of
115
+ * them is, and a send into that group would be refused. The clause names them
116
+ * by handle because that is the spelling `rine_whoami` prints, so the two
117
+ * outputs can be read against each other — a UUID matched nothing this surface
118
+ * had ever shown the model. What neither tool says is which agent this host is
119
+ * acting AS: `WhoAmI` carries no such field and `renderWhoAmI` marks no row, so
120
+ * in a multi-agent org that still has to come from the host's own config (the
121
+ * `agent` this client was built with, which may itself have been given as a
122
+ * UUID).
123
+ */
124
+ function renderGroups(items, handles) {
125
+ if (items.length === 0) return "This agent belongs to no groups.";
126
+ return items.map((g, i) => `${i + 1}. ${renderGroupRow(g, handles)}`).join("\n");
127
+ }
128
+ /**
129
+ * One directory row.
130
+ *
131
+ * No encryption mode: a directory result carries no MLS latch at all, so
132
+ * naming one here would be an invention. `rine_group_inspect` answers that
133
+ * question, and only for a group an agent of this org is already in.
134
+ */
135
+ function renderGroupSummaryRow(g) {
136
+ const desc = g.description ? ` — ${g.description}` : "";
137
+ return `${g.handle} (id ${g.id}) — enrollment ${g.enrollment_policy}, ${g.member_count} member(s)${desc}`;
138
+ }
139
+ /**
140
+ * A numbered list of publicly listed groups, or the empty-state line.
141
+ *
142
+ * The header says what the list IS. `GET /directory/groups` is an
143
+ * unauthenticated public-visibility scan across every org and carries no
144
+ * identity at all, so nothing on a row can say whether this agent holds a seat
145
+ * in that group — while `renderGroups` above prints near-identical rows that
146
+ * end with exactly that. Two numbered lists told apart by two words in a header
147
+ * is not enough, so this one says the difference outright.
148
+ *
149
+ * It names no verb: the header states what the caller is looking at, and which
150
+ * tool to reach for next is the tool description's sentence, not this one's.
151
+ */
152
+ function renderGroupSummaries(items) {
153
+ if (items.length === 0) return "No public groups matched.";
154
+ return [`${items.length} public group(s) found in the directory. A row here is a group that exists, not a group this agent is in:`, ...items.map((g, i) => `${i + 1}. ${renderGroupSummaryRow(g)}`)].join("\n");
155
+ }
156
+ /**
157
+ * One roster row: who they are, what they may do, and since when.
158
+ *
159
+ * The join date renders date-only, and the same way on every surface that
160
+ * renders a roster: `joined_at` is a validated ISO-8601 timestamp on the wire
161
+ * (`GroupMemberSchema`), and a model reading seven surfaces should not have to
162
+ * parse three renderings of the same field.
163
+ *
164
+ * A row of this org's own is suffixed `(yours)`. That is a MARKER, not a
165
+ * filter: every member of the group is listed whichever org holds the seat, and
166
+ * the suffix only says which of them this host could act as.
167
+ */
168
+ function renderMemberRow(m) {
169
+ const joined = m.joined_at.slice(0, 10);
170
+ const own = m.is_own_org ? " (yours)" : "";
171
+ return `${m.agent_handle ?? m.agent_id} — ${m.role}, joined ${joined}${own}`;
172
+ }
173
+ /**
174
+ * A numbered roster, or the empty-state line.
175
+ *
176
+ * The roster is members only. An invitation and a nomination hold a seat
177
+ * against the group's ceiling without appearing here — `rine_group_requests`
178
+ * is what reports those — so a roster shorter than the ceiling suggests is not
179
+ * a gap in this list.
180
+ *
181
+ * Every member is listed, from every org. Nothing here filters on whose agent
182
+ * a row is; `(yours)` marks this org's rows and takes none away.
183
+ */
184
+ function renderRoster(items) {
185
+ if (items.length === 0) return "No members reported for this group.";
186
+ return items.map((m, i) => `${i + 1}. ${renderMemberRow(m)}`).join("\n");
187
+ }
188
+ //#endregion
189
+ //#region src/format.ts
190
+ /**
191
+ * Pure render functions: turn SDK return values into the human-readable strings
192
+ * tools + the channel hand to the LLM / send back over rine.
193
+ *
194
+ * These read ONLY `plaintext` / `decrypt_error` / verification
195
+ * fields. They NEVER read `encrypted_payload` or any envelope/ciphertext field,
196
+ * so ciphertext can never reach the LLM context through a rendered string.
197
+ */
198
+ /** One thread turn as a role-tagged line: `[sent] you: …` / `[received] alice@org: …`. */
199
+ function renderThreadLine(e) {
200
+ const who = e.direction === "sent" ? "you" : e.senderHandle ?? "unknown";
201
+ return `[${e.direction}] ${who}: ${e.text}`;
202
+ }
203
+ /**
204
+ * Render a both-sided transcript (oldest→newest) for `rine_thread`. Each turn is
205
+ * a role-tagged line; `[unavailable]` text passes through unchanged.
206
+ */
207
+ function renderThread(entries) {
208
+ if (entries.length === 0) return "No messages in this conversation.";
209
+ return entries.map(renderThreadLine).join("\n");
210
+ }
211
+ /** Honest signature note — never claims "verified" for an unverifiable message. */
212
+ function verifiedNote(msg) {
213
+ return msg.verified ? "signature verified" : `signature ${msg.verification_status}`;
214
+ }
215
+ /**
216
+ * THE PLAINTEXT-IS-JSON FOOTGUN. Outbound sends wrap `{ text: body }`, and the
217
+ * SDK auto-`JSON.parse`s inbound `application/json` plaintext into a structured
218
+ * value. Unwrap defensively so the model sees prose, never raw JSON:
219
+ * - a string → returned as-is
220
+ * - `{ text: "…" }` → the inner text
221
+ * - anything else → compact JSON (last resort)
222
+ */
223
+ function unwrapText(plaintext) {
224
+ if (typeof plaintext === "string") return plaintext;
225
+ if (plaintext === null || plaintext === void 0) return "";
226
+ if (typeof plaintext === "object") {
227
+ const text = plaintext.text;
228
+ if (typeof text === "string") return text;
229
+ }
230
+ try {
231
+ return JSON.stringify(plaintext);
232
+ } catch {
233
+ return String(plaintext);
234
+ }
235
+ }
236
+ /**
237
+ * Body of a message: decrypt error if unreadable, else the unwrapped plaintext.
238
+ *
239
+ * `explain` is the per-render tiering: pass it on a MULTI render (an inbox,
240
+ * one call per `renderInbox()`) so the no-sender-key cause explains itself in
241
+ * full on the first unreadable row and marks every subsequent one. Omit it on
242
+ * a SINGLE render (`rine_read`, a `rine_send_and_wait` reply, the webhook
243
+ * channel's own inbound render) so the one row keeps the full sentence it
244
+ * already carries — there is nothing to tier when the caller asked for
245
+ * exactly one message. Every other decrypt-error cause is untouched either
246
+ * way; classification is the typed `decrypt_error_code`, never the words in
247
+ * `decrypt_error`.
248
+ */
249
+ function renderMessageBody(msg, explain) {
250
+ if (msg.decrypt_error) {
251
+ const explanation = explain ? senderKeyExplanation(msg) : null;
252
+ return `[unreadable] ${explain && explanation ? explain(explanation).line : msg.decrypt_error}`;
253
+ }
254
+ return unwrapText(msg.plaintext);
255
+ }
256
+ /** Sender label: prefer the human handle, fall back to the agent UUID. */
257
+ function senderLabel(msg) {
258
+ return msg.sender_handle ?? msg.from_agent_id ?? "unknown sender";
259
+ }
260
+ /** A single message rendered across multiple labeled lines (for `rine_read`). */
261
+ function renderSingleMessage(msg) {
262
+ const lines = [
263
+ `Message ${msg.id}`,
264
+ `from: ${senderLabel(msg)}`,
265
+ `type: ${msg.type}`
266
+ ];
267
+ if (msg.group_handle ?? msg.group_id) lines.push(`group: ${msg.group_handle ?? msg.group_id}`);
268
+ lines.push(`body: ${renderMessageBody(msg)}`);
269
+ lines.push(`(${verifiedNote(msg)})`);
270
+ return lines.join("\n");
271
+ }
272
+ /** One inbox row: compact single line keyed by id + sender + body preview. */
273
+ function renderInboxRow(msg, explain) {
274
+ const from = senderLabel(msg);
275
+ const where = msg.group_handle ? ` in ${msg.group_handle}` : "";
276
+ return `${msg.id} from ${from}${where}: ${renderMessageBody(msg, explain)} (${verifiedNote(msg)})`;
277
+ }
278
+ /**
279
+ * A numbered inbox list, or the empty-state line.
280
+ *
281
+ * MULTI render: one explainer per `renderInbox()` call, so the
282
+ * no-sender-key cause explains itself once for this render and marks every
283
+ * later row with the same cause — never module-level, never per-row.
284
+ */
285
+ function renderInbox(items) {
286
+ if (items.length === 0) return "No new messages.";
287
+ const explain = senderKeyExplainer();
288
+ return items.map((msg, i) => `${i + 1}. ${renderInboxRow(msg, explain)}`).join("\n");
289
+ }
290
+ /** One discovery row. */
291
+ function renderAgentSummary(a) {
292
+ const verified = a.verified ? " [verified]" : "";
293
+ const desc = a.description ? ` — ${a.description}` : "";
294
+ const cat = a.category ? ` (${a.category})` : "";
295
+ return `${a.handle}${verified}${cat}${desc}`;
296
+ }
297
+ /** A numbered discovery list, or the empty-state line. */
298
+ function renderDiscover(items) {
299
+ if (items.length === 0) return "No agents matched.";
300
+ return items.map((a, i) => `${i + 1}. ${renderAgentSummary(a)}`).join("\n");
301
+ }
302
+ /** A full agent profile (for `rine_inspect`). */
303
+ function renderProfile(p) {
304
+ const lines = [
305
+ `${p.name} (${p.handle})`,
306
+ `id: ${p.id}`,
307
+ `verified: ${p.verified ? "yes" : "no"}`,
308
+ `human oversight: ${p.human_oversight ? "yes" : "no"}`
309
+ ];
310
+ if (p.category) lines.push(`category: ${p.category}`);
311
+ if (p.description) lines.push(`description: ${p.description}`);
312
+ return lines.join("\n");
313
+ }
314
+ /**
315
+ * This agent's own identity, for `rine_whoami`: the org it belongs to, the
316
+ * trust tier that org holds, and every handle it can act as.
317
+ *
318
+ * Revoked agents are dropped. The route returns them, and naming one here
319
+ * would offer the model a handle nothing can be sent from or to.
320
+ */
321
+ function renderWhoAmI(me) {
322
+ const slug = me.org.slug ? ` (${me.org.slug})` : "";
323
+ const live = me.agents.filter((a) => !a.revoked_at).map((a) => a.handle);
324
+ return [
325
+ `Org ${me.org.name}${slug}`,
326
+ `trust tier: ${me.trust_tier}`,
327
+ `agents: ${live.length > 0 ? live.join(", ") : "none live"}`
328
+ ].join("\n");
329
+ }
330
+ /**
331
+ * A group rendered for `rine_group_inspect`. Every branch is `[OK]` — an MLS
332
+ * group is readable/postable from here.
333
+ *
334
+ * Four states, not two. The second is the MLS init window: the group has no
335
+ * `mls_group_id` to render yet, while the server already accepts nothing but
336
+ * MLS there, so sends from here are MLS sends. Rendering that window as either
337
+ * of the other states says the opposite of what is happening.
338
+ *
339
+ * The third is a group created to run MLS whose ratchet tree was never founded.
340
+ * It reads to every other question exactly like the fourth — an ordinary
341
+ * sender-key group — so it used to fall into it, and the agent was never told
342
+ * the group had not got the encryption it was created for. It stays `[OK]`:
343
+ * every claim that marker makes is true here, the group carries traffic, and
344
+ * only the regime is other than intended. The sentence is the SDK's, and this
345
+ * surface asks for the no-verb spelling because `rine_group_reclaim` refuses on
346
+ * eve — naming a verb that will not act is worse than naming none.
347
+ *
348
+ * `your agents` closes the gap the `[OK]` lines leave open: they describe how
349
+ * the GROUP is run, not whether this org may act in it. A group can be perfectly
350
+ * readable in the abstract and hold no seat of this org's at all. It names them
351
+ * by handle when `handles` resolves them — the rule is stated once on
352
+ * `AgentHandleMap` in the SDK, and `renderOwnAgents` is where it is applied.
353
+ */
354
+ function renderGroup(g, handles) {
355
+ const lines = [`Group ${g.handle} (id ${g.id})`];
356
+ if (groupIsMls(g)) {
357
+ lines.push("[OK] MLS group — end-to-end encrypted (RFC 9420), readable/postable from here.");
358
+ if (g.mls_group_id) lines.push(`mls_group_id: ${g.mls_group_id}`);
359
+ } else if (groupMlsInitInFlight(g)) lines.push("[OK] MLS group, initialising — end-to-end encrypted. Sends from here already use MLS.");
360
+ else if (groupMlsNeverFounded(g)) lines.push(`[OK] sender-key group — readable/postable from here. ${unfoundedGroupNote()}`);
361
+ else lines.push("[OK] sender-key group — readable/postable from here.");
362
+ lines.push(`enrollment: ${g.enrollment_policy}`);
363
+ lines.push(`visibility: ${g.visibility}`);
364
+ lines.push(`conversation_id: ${g.conversation_id ?? "(none — nothing has been said in this group yet)"}`);
365
+ lines.push(`your agents: ${renderOwnAgents(g.member_agent_ids, handles)}`);
366
+ return lines.join("\n");
367
+ }
368
+ /** `rine_group_join` outcome: immediate membership vs a pending vote. */
369
+ function renderJoinResult(target, result) {
370
+ if (result.status === "joined") return `Joined ${target}.`;
371
+ return `Join request for ${target} submitted — pending approval (request ${result.request.id}).`;
372
+ }
373
+ /** One pending-invite row: group, inviter, status, and any message. */
374
+ function renderInviteRow(inv) {
375
+ const group = inv.group_handle ?? inv.group_name ?? inv.group_id;
376
+ const from = inv.invited_by ? ` invited by ${inv.invited_by}` : "";
377
+ const msg = inv.message ? `: "${inv.message}"` : "";
378
+ return `${group} (status ${inv.status})${from}${msg}`;
379
+ }
380
+ /** A numbered list of the caller's pending group invites, or the empty-state line. */
381
+ function renderInvites(items) {
382
+ if (items.length === 0) return "No pending group invites.";
383
+ return items.map((inv, i) => `${i + 1}. ${renderInviteRow(inv)}`).join("\n");
384
+ }
385
+ //#endregion
386
+ //#region src/errors.ts
387
+ /**
388
+ * `formatError(err)` — turns any thrown SDK error into a readable
389
+ * string for the LLM, never a stack trace. Tools wrap their one `await client.*`
390
+ * call in `try/catch → formatError` and RESOLVE (never reject) for mapped errors.
391
+ *
392
+ * Ordering matters: the TS SDK error classes form a hierarchy rooted at
393
+ * `RineApiError`, so this maps MOST-SPECIFIC FIRST and lets `RineApiError` be the
394
+ * catch-all for API errors.
395
+ */
396
+ /** The synchronous message thrown by `sendAndWait` for a `#` group handle. */
397
+ const GROUP_ON_WAIT_PREFIX = "sendAndWait() does not support group handles";
398
+ /**
399
+ * True when `err` is the plain `Error` `sendAndWait` throws for a group handle.
400
+ * The `send_and_wait` tool checks this BEFORE `formatError` and returns the
401
+ * "1:1 only" guidance; it is not a typed SDK error class.
402
+ */
403
+ function isGroupUnsupportedOnWait(err) {
404
+ return err instanceof Error && err.message.startsWith(GROUP_ON_WAIT_PREFIX);
405
+ }
406
+ /** The fixed reply for the group-on-`sendAndWait` case. */
407
+ const GROUP_ON_WAIT_MESSAGE = "rine_send_and_wait is 1:1 only; use rine_send for groups.";
408
+ /**
409
+ * A group 404 has THREE producers on this stack, two spellings, and only one of
410
+ * them is a `NotFoundError`.
411
+ *
412
+ * - the **server** answers a bare `Group not found` (404 → `NotFoundError`);
413
+ * - the **TS SDK** resolves the handle itself before every group send and
414
+ * throws a PLAIN `Error`, `Group not found: <handle>`
415
+ * (`rine-ts-sdk/src/resources/messages.ts` `resolveGroup`;
416
+ * `rine-core`'s `getOrCreateSenderKey` throws the same shape);
417
+ * - the SDK's group **resolver** (`groups.resolveRef`) refuses the same way
418
+ * for every group verb on this surface, and its refusal is a plain `Error`
419
+ * carrying the same lead plus the caller's own roster.
420
+ *
421
+ * 🔴 `err instanceof NotFoundError` is therefore not the test. Keyed on the
422
+ * class alone, the commonest group failure there is — a mistyped handle on
423
+ * `rine_send` — fell past the branch to the bare `String(err.message)` arm with
424
+ * no remedy at all. The predicate reads the MESSAGE, whichever class carried
425
+ * it.
426
+ *
427
+ * This package used to mint a third spelling of its own — `No group '<x>' among
428
+ * the groups your org's agents belong to`, thrown by a local `groups.list()`
429
+ * match — and it went when that local match did. Nothing on this stack
430
+ * produces that sentence any more, so nothing here reads for it: a predicate
431
+ * kept for a producer that no longer exists is a claim that the surface can
432
+ * still say something it cannot.
433
+ *
434
+ * A group needs the group half of the directory: `rine_discover` searches
435
+ * AGENTS and can never resolve a group handle, so pointing a missing group at
436
+ * it sends the caller to a search that structurally cannot answer.
437
+ */
438
+ const GROUP_NOT_FOUND_LEAD = "Group not found";
439
+ /**
440
+ * The SECOND lead, and the one a BARE recipient name now carries.
441
+ *
442
+ * The bare-name miss stopped leading with a group verdict — both namespaces
443
+ * have answered no by then, and the sentence says so — but the REMEDY below is
444
+ * unchanged, and that is the whole reason this constant exists here. Dropping
445
+ * the first lead without adding this one routes every bare-name miss to
446
+ * `formatError`'s catch-all, which names `rine_discover`: the public AGENT
447
+ * directory, which cannot resolve a group handle. That is exactly the
448
+ * misdirection this constant exists to prevent, restored silently, with every
449
+ * suite green.
450
+ *
451
+ * Pinned against `rine-core`, `rine-sdk` and the four other reading surfaces by
452
+ * `NEITHER_NAMESPACE_LEAD_COPIES` (`tests/helpers_client_paths.py`).
453
+ */
454
+ const NEITHER_NAMESPACE_LEAD = "names neither an agent nor a group you can reach";
455
+ /**
456
+ * True for any group-404 spelling, whatever class carried it.
457
+ *
458
+ * 🔴 The second lead is matched by CONTAINMENT, not prefix: its sentence opens
459
+ * with the caller's own spelling (`'kofi' names neither …`), so `startsWith`
460
+ * cannot see it and the arm would read as "not a group problem" forever.
461
+ */
462
+ function isGroupNotFound(text) {
463
+ return text.startsWith(GROUP_NOT_FOUND_LEAD) || text.includes(NEITHER_NAMESPACE_LEAD);
464
+ }
465
+ /**
466
+ * The one remedy all three take.
467
+ *
468
+ * `rine_groups` is named FIRST because it resolves a group the caller's org
469
+ * holds a seat in, public or private. `rine_discover_groups` searches the
470
+ * directory, which the server filters to `visibility == "public"`, so it
471
+ * structurally cannot answer for a private group — it is named second because
472
+ * it is the right tool for finding a public group you have never joined.
473
+ */
474
+ const GROUP_NOT_FOUND_REMEDY = "Try rine_groups to find the right group handle, or rine_discover_groups to search the public directory.";
475
+ /** The half of that remedy that is still worth naming once the roster is in hand. */
476
+ const SEARCH_THE_DIRECTORY = "Try rine_discover_groups to search the public directory.";
477
+ /**
478
+ * A group refusal, plus the tool that can answer it — and one verb fewer when
479
+ * the refusal has already answered itself.
480
+ *
481
+ * The SDK resolves a group reference against the caller's own groups and, on a
482
+ * miss, hands back the list it was holding: {@link GROUP_ROSTER_LEAD} opens the
483
+ * roster, {@link NO_GROUPS_IN_ORG} is the arm for an org that holds a seat
484
+ * nowhere. Printing "run `rine_groups`" underneath either one tells a model to
485
+ * go and fetch what it is already reading — the `rine_discover` defect
486
+ * inverted, and measured to be worse than a wasted call: models parrot the hint
487
+ * back as their answer instead of acting on it. `rine_discover_groups` is then
488
+ * the only verb that can still find something new, so it is the only one named.
489
+ * A **server** group 404 carries no roster and keeps both verbs.
490
+ *
491
+ * The roster arm ends on a LIST ROW, not on a sentence, so the clause joins
492
+ * with a newline and the lead's own full stop is dropped: `(Planning).` reads
493
+ * as part of that last group's entry, and a caller copying the row it was told
494
+ * to name copies the period into the retry.
495
+ *
496
+ * Both sentences are imported from the SDK rather than retyped, so a reword
497
+ * there cannot leave a stale literal here quietly restoring the loop.
498
+ */
499
+ function groupNotFoundMessage(detail) {
500
+ if (detail.includes(GROUP_ROSTER_LEAD)) return `Not found: ${detail}\n${SEARCH_THE_DIRECTORY}`;
501
+ if (detail.includes(NO_GROUPS_IN_ORG)) return `Not found: ${detail} ${SEARCH_THE_DIRECTORY}`;
502
+ return `Not found: ${detail.replace(/\.+$/, "")}. ${GROUP_NOT_FOUND_REMEDY}`;
503
+ }
504
+ function formatError(err) {
505
+ if (err instanceof ZodError) {
506
+ const first = err.issues[0];
507
+ return `Invalid input: ${first?.path.length ? first.path.join(".") : "input"} — ${first?.message ?? "validation failed"}.`;
508
+ }
509
+ if (err instanceof AuthenticationError) return "rine auth failed — onboard with `npx @rine-network/eve onboard` or set RINE_CONFIG_DIR to your credentials directory.";
510
+ if (err instanceof AuthorizationError) return `Not authorized: ${err.detail}.`;
511
+ if (err instanceof NotFoundError) {
512
+ if (isGroupNotFound(err.detail)) return groupNotFoundMessage(err.detail);
513
+ return `Not found: ${err.detail}. Try rine_discover to find the right handle.`;
514
+ }
515
+ if (err instanceof RateLimitError) return `Rate-limited; retry after ${err.retryAfter ?? "a few"}s.`;
516
+ if (err instanceof ValidationError) return `Invalid input: ${err.detail}.`;
517
+ if (err instanceof SchemaValidationError) return `Invalid input: ${err.message}.`;
518
+ if (err instanceof RineTimeoutError) return "Request timed out; try again or raise the timeout.";
519
+ if (err instanceof ConfigError) return String(err.message);
520
+ if (err instanceof CryptoError) return `Encryption error: ${err.message} (check your agent keys are on disk).`;
521
+ if (err instanceof APIConnectionError) return `Could not reach rine: ${err.message}.`;
522
+ if (err instanceof RineApiError) return `${err.status}: ${err.detail}`;
523
+ const message = String(messageOf(err));
524
+ if (isGroupNotFound(message)) return groupNotFoundMessage(message);
525
+ return message;
526
+ }
527
+ function messageOf(err) {
528
+ if (err instanceof Error) return err.message;
529
+ return String(err ?? "unknown error");
530
+ }
531
+ //#endregion
532
+ //#region src/tool.ts
533
+ /**
534
+ * Shared `defineTool` plumbing every rine tool reuses (no duplicated logic):
535
+ * env-based identity resolution and the try/catch → `formatError` wrapper
536
+ * (errors→strings, never a reject).
537
+ *
538
+ * Unlike the Mastra integration (which threads identity through a `RequestContext`),
539
+ * Eve tools run in the app runtime with full `process.env` access, so identity is
540
+ * resolved straight from the environment via `getRineClient` — no per-call context
541
+ * plumbing, no credentials in the model-visible input schema.
542
+ *
543
+ * A tool's per-tool body is just `(client, input, apiUrl) => string`; this helper
544
+ * supplies the surrounding contract. The concrete `defineTool({...})` (with the
545
+ * model-facing description + schemas) lives in each domain module; this file only
546
+ * builds the `execute` closure and the shared option/body types.
547
+ */
548
+ /**
549
+ * Resolve {@link RineToolOpts.needsApproval} to an Eve `needsApproval` callback,
550
+ * or `undefined` (no gate) when unset. Only the mutating tool factories apply it.
551
+ */
552
+ function approvalGate(opts) {
553
+ switch (opts.needsApproval) {
554
+ case "always": return always();
555
+ case "once": return once();
556
+ case "never": return never();
557
+ default: return;
558
+ }
559
+ }
560
+ /** Resolve the effective `AsyncRineClient` for one `execute` call (lazy). */
561
+ function resolveClient(opts) {
562
+ if (opts.client) {
563
+ const agent = actingAgent(opts);
564
+ return agent ? opts.client.withAgent(agent) : opts.client;
565
+ }
566
+ return getRineClient({
567
+ configDir: opts.configDir,
568
+ apiUrl: opts.apiUrl,
569
+ agent: actingAgent(opts)
570
+ });
571
+ }
572
+ /**
573
+ * The same resolved `apiUrl` the client is built against (factory override →
574
+ * `RINE_API_URL` env → `resolveApiUrl()`). Exposed so handle→UUID pre-resolution
575
+ * hits the SAME server WebFinger the client uses.
576
+ */
577
+ function resolveApiUrlFor(opts) {
578
+ return opts.apiUrl ?? resolveApiUrl();
579
+ }
580
+ /** Brand a model-supplied recipient string for the SDK send surface. */
581
+ function asRecipient(to) {
582
+ return to;
583
+ }
584
+ /**
585
+ * Belt-and-suspenders ciphertext redactor for `read` / `inbox` / `thread`.
586
+ * The renderers + `outputSchema: z.string()` already guarantee the `execute`
587
+ * return is redacted text; this coerces ANY value to plain text before it can
588
+ * reach the model, in Eve's `ToolModelOutput` shape.
589
+ */
590
+ function redactToText(output) {
591
+ return {
592
+ type: "text",
593
+ value: typeof output === "string" ? output : ""
594
+ };
595
+ }
596
+ /**
597
+ * Build an Eve-tool `execute(input, ctx)` from a zod schema + body, applying:
598
+ * 1. zod `.parse(raw)` of the model input (defaults + coercion + validation),
599
+ * 2. the lazy env-resolved client,
600
+ * 3. the try/catch→formatError contract (resolves a string, never rejects).
601
+ *
602
+ * Eve hands `execute` the raw model input as `Record<string, unknown>`; we own the
603
+ * parse because Eve receives a JSON Schema (not the zod schema) and may not apply
604
+ * zod defaults. The Eve `ToolContext` is intentionally ignored — rine identity is
605
+ * environment-resolved, not session-scoped.
606
+ */
607
+ function makeExecute(schema, opts, body) {
608
+ return async (raw) => {
609
+ try {
610
+ const input = schema.parse(raw);
611
+ return await body(resolveClient(opts), input, resolveApiUrlFor(opts));
612
+ } catch (err) {
613
+ return formatError(err);
614
+ }
615
+ };
616
+ }
617
+ //#endregion
618
+ export { renderRoster as C, renderGroups as S, renderThreadLine as _, GROUP_ON_WAIT_MESSAGE as a, verifiedNote as b, renderDiscover as c, renderInvites as d, renderJoinResult as f, renderThread as g, renderSingleMessage as h, redactToText as i, renderGroup as l, renderProfile as m, asRecipient as n, formatError as o, renderMessageBody as p, makeExecute as r, isGroupUnsupportedOnWait as s, approvalGate as t, renderInbox as u, renderWhoAmI as v, renderGroupSummaries as x, senderLabel as y };
package/dist/tool.d.ts CHANGED
@@ -57,14 +57,14 @@ export declare function resolveClient(opts: RineToolOpts): AsyncRineClient;
57
57
  export declare function resolveApiUrlFor(opts: RineToolOpts): string;
58
58
  /**
59
59
  * The recipient union `client.send`/`sendAndWait` accept. The model supplies a
60
- * free-form `to` string (a `name@org` handle, a `#group@org` handle, or a UUID);
60
+ * free-form `to` string (a `name@org` handle, a `#logistics@acme.rine.network` handle, or a UUID);
61
61
  * the SDK normalizes + validates it at runtime, so this brands the string once.
62
62
  */
63
63
  type Recipient = AgentHandle | AgentUuid | GroupHandle | GroupUuid;
64
64
  /** Brand a model-supplied recipient string for the SDK send surface. */
65
65
  export declare function asRecipient(to: string): Recipient;
66
66
  /**
67
- * Belt-and-suspenders ciphertext redactor for `read` / `check_inbox`.
67
+ * Belt-and-suspenders ciphertext redactor for `read` / `inbox` / `thread`.
68
68
  * The renderers + `outputSchema: z.string()` already guarantee the `execute`
69
69
  * return is redacted text; this coerces ANY value to plain text before it can
70
70
  * reach the model, in Eve's `ToolModelOutput` shape.
@@ -1,9 +1,15 @@
1
1
  /**
2
- * The 2 discovery tool factories: `rine_discover`, `rine_inspect`. Both are
3
- * unauthenticated directory reads.
2
+ * The 4 discovery tool factories. `rine_discover` and `rine_inspect` are
3
+ * unauthenticated directory reads; `rine_whoami` reads this agent's OWN
4
+ * identity, so it is the one authenticated verb here.
5
+ *
6
+ * `rine_discover_groups` is the directory's group half and lives with the other
7
+ * group readers in `groups-list.ts`; only its registry entry says `discovery`.
4
8
  */
5
9
  import { type RineToolOpts } from "../tool.js";
6
10
  /** `rine_discover` — search the public agent directory. */
7
11
  export declare function rineDiscoverTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
8
12
  /** `rine_inspect` — fetch one agent's full public profile. */
9
13
  export declare function rineInspectTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;
14
+ /** `rine_whoami` — this agent's own org, trust tier, and live handles. */
15
+ export declare function rineWhoamiTool(opts?: RineToolOpts): import("eve/tools").ToolDefinition<any, any>;