@rine-network/cli 0.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/main.js +1571 -741
  3. package/package.json +17 -5
package/dist/main.js CHANGED
@@ -2,8 +2,25 @@ import { createRequire } from "node:module";
2
2
  import { Command } from "commander";
3
3
  import * as fs from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { ed25519, x25519 } from "@noble/curves/ed25519.js";
5
6
  import readline from "node:readline";
7
+ import { Aes256Gcm, CipherSuite, HkdfSha256 } from "@hpke/core";
8
+ import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
9
+ import { hmac } from "@noble/hashes/hmac.js";
10
+ import { sha256 } from "@noble/hashes/sha2.js";
6
11
  import { createEventSource } from "eventsource-client";
12
+ //#region \0rolldown/runtime.js
13
+ var __defProp = Object.defineProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
23
+ //#endregion
7
24
  //#region src/errors.ts
8
25
  var RineApiError = class extends Error {
9
26
  constructor(status, detail, raw) {
@@ -67,6 +84,14 @@ function saveTokenCache(cache) {
67
84
  ensureDir(dir);
68
85
  writeAtomic(join(dir, "token_cache.json"), JSON.stringify(cache, null, 2));
69
86
  }
87
+ function cacheToken(profile, token) {
88
+ const cache = loadTokenCache();
89
+ cache[profile] = {
90
+ access_token: token.access_token,
91
+ expires_at: Date.now() / 1e3 + token.expires_in
92
+ };
93
+ saveTokenCache(cache);
94
+ }
70
95
  /**
71
96
  * Resolves credentials for a profile. Priority:
72
97
  * 1. RINE_CLIENT_ID + RINE_CLIENT_SECRET env vars (both required)
@@ -96,10 +121,12 @@ var HttpClient = class {
96
121
  baseUrl;
97
122
  tokenFn;
98
123
  canRefresh;
124
+ defaultHeaders;
99
125
  constructor(opts) {
100
- this.baseUrl = (opts.apiUrl ?? getApiUrl()).replace(/\/$/, "");
126
+ this.baseUrl = opts.apiUrl ?? getApiUrl();
101
127
  this.tokenFn = opts.tokenFn;
102
128
  this.canRefresh = opts.canRefresh ?? true;
129
+ this.defaultHeaders = opts.defaultHeaders ?? {};
103
130
  }
104
131
  async request(method, path, body, params, extraHeaders) {
105
132
  let url = this.baseUrl + path;
@@ -110,6 +137,7 @@ var HttpClient = class {
110
137
  const doFetch = async (force) => {
111
138
  const headers = {
112
139
  Authorization: `Bearer ${await this.tokenFn(force)}`,
140
+ ...this.defaultHeaders,
113
141
  ...extraHeaders
114
142
  };
115
143
  if (body !== void 0) headers["Content-Type"] = "application/json";
@@ -129,20 +157,32 @@ var HttpClient = class {
129
157
  if (res.status === 204) return void 0;
130
158
  return res.json();
131
159
  }
132
- get(path, params) {
133
- return this.request("GET", path, void 0, params);
160
+ get(path, params, extraHeaders) {
161
+ return this.request("GET", path, void 0, params, extraHeaders);
134
162
  }
135
163
  post(path, body, extraHeaders) {
136
164
  return this.request("POST", path, body, void 0, extraHeaders);
137
165
  }
138
- put(path, body) {
139
- return this.request("PUT", path, body);
166
+ put(path, body, extraHeaders) {
167
+ return this.request("PUT", path, body, void 0, extraHeaders);
168
+ }
169
+ patch(path, body, extraHeaders) {
170
+ return this.request("PATCH", path, body, void 0, extraHeaders);
140
171
  }
141
- patch(path, body) {
142
- return this.request("PATCH", path, body);
172
+ delete(path, extraHeaders) {
173
+ return this.request("DELETE", path, void 0, void 0, extraHeaders);
143
174
  }
144
- delete(path) {
145
- return this.request("DELETE", path);
175
+ /** Unauthenticated GET for public endpoints (e.g. /directory/*). */
176
+ static async publicGet(path, params) {
177
+ const base = getApiUrl();
178
+ const qs = params?.toString();
179
+ const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
180
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
181
+ if (!res.ok) {
182
+ const detail = await parseErrorDetail(res);
183
+ throw new RineApiError(res.status, detail, res);
184
+ }
185
+ return res.json();
146
186
  }
147
187
  };
148
188
  /**
@@ -151,6 +191,27 @@ var HttpClient = class {
151
191
  * 2. Token cache — returned if not expired (60s margin)
152
192
  * 3. POST /oauth/token — fetches fresh token and caches it
153
193
  */
194
+ /**
195
+ * Fetches an OAuth token from POST /oauth/token.
196
+ * Shared by getOrRefreshToken, login, and register.
197
+ */
198
+ async function fetchOAuthToken(clientId, clientSecret) {
199
+ const apiUrl = getApiUrl();
200
+ const res = await fetch(`${apiUrl}/oauth/token`, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
203
+ body: new URLSearchParams({
204
+ grant_type: "client_credentials",
205
+ client_id: clientId,
206
+ client_secret: clientSecret
207
+ }).toString()
208
+ });
209
+ if (!res.ok) {
210
+ const body = await res.json().catch(() => ({}));
211
+ throw new RineApiError(res.status, body.detail ?? "Token request failed");
212
+ }
213
+ return res.json();
214
+ }
154
215
  async function getOrRefreshToken(entry, profileName, force = false) {
155
216
  const envToken = process.env.RINE_TOKEN;
156
217
  if (envToken) return envToken;
@@ -160,28 +221,11 @@ async function getOrRefreshToken(entry, profileName, force = false) {
160
221
  if (cached !== void 0 && cached.expires_at - now > 60) return cached.access_token;
161
222
  }
162
223
  if (!entry) throw new RineApiError(0, "No credentials found. Run `rine login` first.");
163
- const apiUrl = getApiUrl();
164
- const res = await fetch(`${apiUrl}/oauth/token`, {
165
- method: "POST",
166
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
167
- body: new URLSearchParams({
168
- grant_type: "client_credentials",
169
- client_id: entry.client_id,
170
- client_secret: entry.client_secret
171
- }).toString()
172
- });
173
- if (!res.ok) throw new RineApiError(res.status, "Token refresh failed");
174
- const data = await res.json();
175
- const freshNow = Date.now() / 1e3;
176
- const cache = loadTokenCache();
177
- cache[profileName] = {
178
- access_token: data.access_token,
179
- expires_at: freshNow + data.expires_in
180
- };
181
- saveTokenCache(cache);
224
+ const data = await fetchOAuthToken(entry.client_id, entry.client_secret);
225
+ cacheToken(profileName, data);
182
226
  return data.access_token;
183
227
  }
184
- async function createClient(profileFlag) {
228
+ async function createClient(profileFlag, defaultHeaders) {
185
229
  const profileName = profileFlag ?? "default";
186
230
  const entry = getCredentialEntry(profileName);
187
231
  const canRefresh = !process.env.RINE_TOKEN && entry !== void 0;
@@ -189,7 +233,8 @@ async function createClient(profileFlag) {
189
233
  return {
190
234
  client: new HttpClient({
191
235
  tokenFn,
192
- canRefresh
236
+ canRefresh,
237
+ defaultHeaders
193
238
  }),
194
239
  profileName,
195
240
  entry
@@ -234,31 +279,45 @@ function printText(msg) {
234
279
  function printError(msg) {
235
280
  console.error(msg);
236
281
  }
237
- function printResult(data, opts) {
238
- if (opts.json) {
239
- printJson(data);
240
- return;
241
- }
242
- if (opts.table && Array.isArray(data)) {
243
- printTable(data);
244
- return;
245
- }
246
- if (Array.isArray(data)) for (const item of data) {
247
- printObject(item);
248
- console.log("---");
249
- }
250
- else printObject(data);
282
+ //#endregion
283
+ //#region src/action.ts
284
+ /**
285
+ * Wraps a command action with automatic client creation and error handling.
286
+ * Commander calls action(fn) with positional args first, then the options object.
287
+ * This wrapper forwards all arguments to the inner function.
288
+ */
289
+ function withClient(program, fn) {
290
+ return async (...args) => {
291
+ try {
292
+ const gOpts = program.opts();
293
+ const defaultHeaders = {};
294
+ if (gOpts.as) defaultHeaders["X-Rine-Agent"] = gOpts.as;
295
+ const { client, profileName } = await createClient(gOpts.profile, defaultHeaders);
296
+ await fn({
297
+ client,
298
+ gOpts,
299
+ profileName,
300
+ extraHeaders: defaultHeaders
301
+ }, ...args);
302
+ } catch (err) {
303
+ printError(formatError(err));
304
+ process.exitCode = 1;
305
+ }
306
+ };
251
307
  }
252
- function printObject(data) {
253
- if (data === null || data === void 0) return;
254
- if (typeof data !== "object") {
255
- console.log(String(data));
256
- return;
257
- }
258
- for (const [k, v] of Object.entries(data)) {
259
- const val = typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
260
- console.log(`${k}: ${val}`);
261
- }
308
+ /**
309
+ * Wraps a command action with error handling only (no client creation).
310
+ * For commands like logout that don't need authentication.
311
+ */
312
+ function withErrorHandler(program, fn) {
313
+ return async (...args) => {
314
+ try {
315
+ await fn(program.opts(), ...args);
316
+ } catch (err) {
317
+ printError(formatError(err));
318
+ process.exitCode = 1;
319
+ }
320
+ };
262
321
  }
263
322
  //#endregion
264
323
  //#region src/commands/agent-profile.ts
@@ -307,6 +366,11 @@ function toRow(card) {
307
366
  Updated: String(card.updated_at ?? "—")
308
367
  };
309
368
  }
369
+ async function updateCard(client, agentId, modify) {
370
+ const card = await client.get(`/agents/${agentId}/card`);
371
+ const updated = modify(card) ?? card;
372
+ return await client.put(`/agents/${agentId}/card`, stripReadOnly(updated));
373
+ }
310
374
  function getAgentCmd(program) {
311
375
  const cmd = program.commands.find((c) => c.name() === "agent");
312
376
  if (!cmd) throw new Error("registerAgent must be called before registerAgentProfile");
@@ -314,36 +378,20 @@ function getAgentCmd(program) {
314
378
  }
315
379
  function registerAgentProfile(program) {
316
380
  const agent = getAgentCmd(program);
317
- agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(async (agentId) => {
318
- try {
319
- const gOpts = program.opts();
320
- const { client } = await createClient(gOpts.profile);
321
- const card = await client.get(`/agents/${agentId}/card`);
322
- if (gOpts.json) printJson(card);
323
- else printTable([toRow(card)]);
324
- } catch (err) {
325
- printError(formatError(err));
326
- process.exitCode = 1;
327
- }
328
- });
329
- agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(async (agentId, opts) => {
330
- try {
331
- const gOpts = program.opts();
332
- const { client } = await createClient(gOpts.profile);
333
- const card = await client.get(`/agents/${agentId}/card`);
381
+ agent.command("profile").description("Get agent card").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
382
+ const card = await client.get(`/agents/${agentId}/card`);
383
+ if (gOpts.json) printJson(card);
384
+ else printTable([toRow(card)]);
385
+ }));
386
+ agent.command("describe").description("Update agent description").argument("<agent-id>", "Agent ID").requiredOption("--description <desc>", "Agent description").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
387
+ const result = await updateCard(client, agentId, (card) => {
334
388
  card.description = opts.description;
335
- await client.put(`/agents/${agentId}/card`, stripReadOnly(card));
336
- printMutationOk("Description updated", gOpts.json);
337
- } catch (err) {
338
- printError(formatError(err));
339
- process.exitCode = 1;
340
- }
341
- });
342
- agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(async (agentId, opts) => {
343
- try {
344
- const gOpts = program.opts();
345
- const { client } = await createClient(gOpts.profile);
346
- const card = await client.get(`/agents/${agentId}/card`);
389
+ });
390
+ if (gOpts.json) printJson(result);
391
+ else printSuccess("Description updated");
392
+ }));
393
+ agent.command("add-skill").description("Add a skill to the agent card").argument("<agent-id>", "Agent ID").requiredOption("--skill-id <id>", "Skill ID").requiredOption("--skill-name <name>", "Skill name").option("--skill-description <desc>", "Skill description").option("--tags <tags>", "Comma-separated tags").option("--examples <examples>", "Comma-separated examples").option("--input-modes <modes>", "Comma-separated input modes").option("--output-modes <modes>", "Comma-separated output modes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
394
+ const result = await updateCard(client, agentId, (card) => {
347
395
  const skill = {
348
396
  id: opts.skillId,
349
397
  name: opts.skillName
@@ -354,69 +402,153 @@ function registerAgentProfile(program) {
354
402
  if (opts.inputModes) skill.inputModes = opts.inputModes.split(",").map((m) => m.trim());
355
403
  if (opts.outputModes) skill.outputModes = opts.outputModes.split(",").map((m) => m.trim());
356
404
  card.skills = [...Array.isArray(card.skills) ? card.skills : [], skill];
357
- await client.put(`/agents/${agentId}/card`, stripReadOnly(card));
358
- printMutationOk("Skill added", gOpts.json);
359
- } catch (err) {
360
- printError(formatError(err));
361
- process.exitCode = 1;
362
- }
363
- });
364
- agent.command("set-categories").description("Set agent categories").argument("<agent-id>", "Agent ID").requiredOption("--categories <cats>", "Comma-separated category names").action(async (agentId, opts) => {
365
- try {
366
- const gOpts = program.opts();
367
- const { client } = await createClient(gOpts.profile);
368
- const card = await client.get(`/agents/${agentId}/card`);
369
- const cats = opts.categories.split(",").map((c) => c.trim());
370
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "categories", cats)));
371
- printMutationOk("Categories updated", gOpts.json);
372
- } catch (err) {
373
- printError(formatError(err));
374
- process.exitCode = 1;
375
- }
376
- });
377
- agent.command("set-languages").description("Set agent supported languages").argument("<agent-id>", "Agent ID").requiredOption("--languages <langs>", "Comma-separated language codes").action(async (agentId, opts) => {
378
- try {
379
- const gOpts = program.opts();
380
- const { client } = await createClient(gOpts.profile);
381
- const card = await client.get(`/agents/${agentId}/card`);
382
- const langs = opts.languages.split(",").map((l) => l.trim());
383
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "languages", langs)));
384
- printMutationOk("Languages updated", gOpts.json);
385
- } catch (err) {
386
- printError(formatError(err));
387
- process.exitCode = 1;
388
- }
389
- });
390
- agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(async (agentId, opts) => {
391
- try {
392
- const gOpts = program.opts();
393
- if (!VALID_PRICING.includes(opts.model)) {
394
- printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
395
- process.exitCode = 2;
396
- return;
397
- }
398
- const { client } = await createClient(gOpts.profile);
399
- const card = await client.get(`/agents/${agentId}/card`);
400
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "pricing_model", opts.model)));
401
- printMutationOk("Pricing updated", gOpts.json);
402
- } catch (err) {
403
- printError(formatError(err));
404
- process.exitCode = 1;
405
+ });
406
+ if (gOpts.json) printJson(result);
407
+ else printSuccess("Skill added");
408
+ }));
409
+ agent.command("set-categories").description("Set agent categories").argument("<agent-id>", "Agent ID").requiredOption("--categories <cats>", "Comma-separated category names").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
410
+ const cats = opts.categories.split(",").map((c) => c.trim());
411
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "categories", cats));
412
+ if (gOpts.json) printJson(result);
413
+ else printSuccess("Categories updated");
414
+ }));
415
+ agent.command("set-languages").description("Set agent supported languages").argument("<agent-id>", "Agent ID").requiredOption("--languages <langs>", "Comma-separated language codes").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
416
+ const langs = opts.languages.split(",").map((l) => l.trim());
417
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "languages", langs));
418
+ if (gOpts.json) printJson(result);
419
+ else printSuccess("Languages updated");
420
+ }));
421
+ agent.command("set-pricing").description("Set agent pricing model").argument("<agent-id>", "Agent ID").requiredOption("--model <model>", `Pricing model (${VALID_PRICING.join("|")})`).action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
422
+ if (!VALID_PRICING.includes(opts.model)) {
423
+ printError(`Invalid pricing model '${opts.model}'. Valid: ${VALID_PRICING.join(", ")}`);
424
+ process.exitCode = 2;
425
+ return;
405
426
  }
427
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "pricing_model", opts.model));
428
+ if (gOpts.json) printJson(result);
429
+ else printSuccess("Pricing updated");
430
+ }));
431
+ agent.command("accept-types").description("Set accepted message types").argument("<agent-id>", "Agent ID").requiredOption("--types <types>", "Comma-separated message types").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
432
+ const types = opts.types.split(",").map((t) => t.trim());
433
+ const result = await updateCard(client, agentId, (card) => setRineField(card, "message_types_accepted", types));
434
+ if (gOpts.json) printJson(result);
435
+ else printSuccess("Accept types updated");
436
+ }));
437
+ }
438
+ //#endregion
439
+ //#region src/crypto/keys.ts
440
+ var keys_exports = /* @__PURE__ */ __exportAll({
441
+ agentIdFromKid: () => agentIdFromKid,
442
+ agentKeysExist: () => agentKeysExist,
443
+ encryptionPublicKeyToJWK: () => encryptionPublicKeyToJWK,
444
+ fromBase64Url: () => fromBase64Url,
445
+ generateAgentKeys: () => generateAgentKeys,
446
+ generateEncryptionKeyPair: () => generateEncryptionKeyPair,
447
+ generateSigningKeyPair: () => generateSigningKeyPair,
448
+ jwkToPublicKey: () => jwkToPublicKey,
449
+ loadAgentKeys: () => loadAgentKeys,
450
+ saveAgentKeys: () => saveAgentKeys,
451
+ signingPublicKeyToJWK: () => signingPublicKeyToJWK,
452
+ toBase64Url: () => toBase64Url,
453
+ validatePathId: () => validatePathId
454
+ });
455
+ function toBase64Url(bytes) {
456
+ return Buffer.from(bytes).toString("base64url");
457
+ }
458
+ function fromBase64Url(s) {
459
+ return new Uint8Array(Buffer.from(s, "base64url"));
460
+ }
461
+ function generateSigningKeyPair() {
462
+ const privateKey = ed25519.utils.randomSecretKey();
463
+ return {
464
+ privateKey,
465
+ publicKey: ed25519.getPublicKey(privateKey)
466
+ };
467
+ }
468
+ function generateEncryptionKeyPair() {
469
+ const privateKey = x25519.utils.randomSecretKey();
470
+ return {
471
+ privateKey,
472
+ publicKey: x25519.getPublicKey(privateKey)
473
+ };
474
+ }
475
+ function generateAgentKeys() {
476
+ return {
477
+ signing: generateSigningKeyPair(),
478
+ encryption: generateEncryptionKeyPair()
479
+ };
480
+ }
481
+ function signingPublicKeyToJWK(publicKey) {
482
+ return {
483
+ kty: "OKP",
484
+ crv: "Ed25519",
485
+ x: toBase64Url(publicKey)
486
+ };
487
+ }
488
+ function encryptionPublicKeyToJWK(publicKey) {
489
+ return {
490
+ kty: "OKP",
491
+ crv: "X25519",
492
+ x: toBase64Url(publicKey)
493
+ };
494
+ }
495
+ function jwkToPublicKey(jwk) {
496
+ const key = fromBase64Url(jwk.x);
497
+ if (key.length !== 32) throw new Error(`Invalid public key: expected 32 bytes, got ${key.length}`);
498
+ return key;
499
+ }
500
+ const KID_RE = /^rine:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
501
+ function agentIdFromKid(kid) {
502
+ const match = KID_RE.exec(kid);
503
+ if (!match) throw new Error(`Invalid sender KID format: ${kid}`);
504
+ return match[1];
505
+ }
506
+ const PATH_SAFE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
507
+ function validatePathId(id, label) {
508
+ if (!PATH_SAFE_RE.test(id)) throw new Error(`Invalid ${label}: must be a UUID, got '${id}'`);
509
+ }
510
+ function getKeysDir$1() {
511
+ return join(getConfigDir(), "keys");
512
+ }
513
+ function agentKeysDir(agentId) {
514
+ validatePathId(agentId, "agent ID");
515
+ return join(getKeysDir$1(), agentId);
516
+ }
517
+ function saveAgentKeys(agentId, keys) {
518
+ const dir = agentKeysDir(agentId);
519
+ fs.mkdirSync(dir, {
520
+ recursive: true,
521
+ mode: 448
406
522
  });
407
- agent.command("accept-types").description("Set accepted message types").argument("<agent-id>", "Agent ID").requiredOption("--types <types>", "Comma-separated message types").action(async (agentId, opts) => {
408
- try {
409
- const gOpts = program.opts();
410
- const { client } = await createClient(gOpts.profile);
411
- const card = await client.get(`/agents/${agentId}/card`);
412
- const types = opts.types.split(",").map((t) => t.trim());
413
- await client.put(`/agents/${agentId}/card`, stripReadOnly(setRineField(card, "message_types_accepted", types)));
414
- printMutationOk("Accept types updated", gOpts.json);
415
- } catch (err) {
416
- printError(formatError(err));
417
- process.exitCode = 1;
523
+ const sigPath = join(dir, "signing.key");
524
+ fs.writeFileSync(sigPath, toBase64Url(keys.signing.privateKey), "utf-8");
525
+ fs.chmodSync(sigPath, 384);
526
+ const encPath = join(dir, "encryption.key");
527
+ fs.writeFileSync(encPath, toBase64Url(keys.encryption.privateKey), "utf-8");
528
+ fs.chmodSync(encPath, 384);
529
+ }
530
+ function loadAgentKeys(agentId) {
531
+ const dir = agentKeysDir(agentId);
532
+ const sigPriv = fromBase64Url(fs.readFileSync(join(dir, "signing.key"), "utf-8").trim());
533
+ if (sigPriv.length !== 32) throw new Error(`Corrupt signing key: expected 32 bytes, got ${sigPriv.length}`);
534
+ const sigPub = ed25519.getPublicKey(sigPriv);
535
+ const encPriv = fromBase64Url(fs.readFileSync(join(dir, "encryption.key"), "utf-8").trim());
536
+ if (encPriv.length !== 32) throw new Error(`Corrupt encryption key: expected 32 bytes, got ${encPriv.length}`);
537
+ const encPub = x25519.getPublicKey(encPriv);
538
+ return {
539
+ signing: {
540
+ privateKey: sigPriv,
541
+ publicKey: sigPub
542
+ },
543
+ encryption: {
544
+ privateKey: encPriv,
545
+ publicKey: encPub
418
546
  }
419
- });
547
+ };
548
+ }
549
+ function agentKeysExist(agentId) {
550
+ const dir = agentKeysDir(agentId);
551
+ return fs.existsSync(join(dir, "signing.key")) && fs.existsSync(join(dir, "encryption.key"));
420
552
  }
421
553
  //#endregion
422
554
  //#region src/prompt.ts
@@ -466,112 +598,96 @@ async function promptConfirm(question) {
466
598
  //#region src/commands/agent.ts
467
599
  function toAgentRow(a, opts = {}) {
468
600
  const row = {
469
- ID: String(a.id ?? ""),
470
- Name: String(a.name ?? ""),
471
- Handle: String(a.handle ?? ""),
472
- "Human Oversight": String(a.human_oversight ?? ""),
473
- Created: String(a.created_at ?? "")
601
+ ID: a.id,
602
+ Name: a.name,
603
+ Handle: a.handle,
604
+ "Human Oversight": String(a.human_oversight),
605
+ Created: a.created_at
474
606
  };
475
- if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] = String(a.verification_words);
607
+ if (opts.includeVerificationWords && a.verification_words) row["Verification Words"] = a.verification_words;
476
608
  if (opts.includeCard) row.Card = a.unlisted ? "unlisted" : "public";
477
609
  if (opts.includeRevoked) row.Revoked = a.revoked_at ? String(a.revoked_at) : "—";
478
610
  return row;
479
611
  }
480
612
  function registerAgent(program) {
481
613
  const agent = program.command("agent").description("Agent management");
482
- agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--human-oversight <bool>", "Require human oversight (true/false)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(async (opts) => {
483
- try {
484
- const gOpts = program.opts();
485
- let name = opts.name;
614
+ agent.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight (default: true)").option("--unlisted", "Mark agent as unlisted (not in public directory)").action(withClient(program, async ({ client, gOpts }, opts) => {
615
+ let name = opts.name;
616
+ if (!name) {
617
+ name = await promptText("Agent name: ");
486
618
  if (!name) {
487
- name = await promptText("Agent name: ");
488
- if (!name) {
489
- printError("Agent name is required");
490
- process.exitCode = 2;
491
- return;
492
- }
493
- }
494
- const body = { name };
495
- if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
496
- if (opts.unlisted) body.unlisted = true;
497
- const { client } = await createClient(gOpts.profile);
498
- const data = await client.post("/agents", body);
499
- if (gOpts.json) printJson(data);
500
- else {
501
- printTable([toAgentRow(data, {
502
- includeCard: true,
503
- includeVerificationWords: true
504
- })]);
505
- const handle = String(data.handle ?? "");
506
- const name = String(data.name ?? "");
507
- console.log(`
508
- Agent '${name}' created — reachable at ${handle}`);
509
- if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
619
+ printError("Agent name is required");
620
+ process.exitCode = 2;
621
+ return;
510
622
  }
511
- } catch (err) {
512
- printError(formatError(err));
513
- process.exitCode = 1;
514
623
  }
515
- });
516
- agent.command("list").description("List all agents").option("--include-revoked", "Include revoked agents").action(async (opts) => {
517
- try {
518
- const gOpts = program.opts();
519
- const { client } = await createClient(gOpts.profile);
520
- const params = opts.includeRevoked ? { include_revoked: true } : void 0;
521
- const data = await client.get("/agents", params);
522
- if (gOpts.json) printJson(data);
523
- else printTable(data.items.map((a) => toAgentRow(a, { includeRevoked: opts.includeRevoked })));
524
- } catch (err) {
525
- printError(formatError(err));
526
- process.exitCode = 1;
624
+ const agentKeys = generateAgentKeys();
625
+ const body = {
626
+ name,
627
+ signing_public_key: signingPublicKeyToJWK(agentKeys.signing.publicKey),
628
+ encryption_public_key: encryptionPublicKeyToJWK(agentKeys.encryption.publicKey)
629
+ };
630
+ if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
631
+ if (opts.unlisted) body.unlisted = true;
632
+ const data = await client.post("/agents", body);
633
+ saveAgentKeys(data.id, agentKeys);
634
+ if (gOpts.json) printJson(data);
635
+ else {
636
+ printTable([toAgentRow(data, {
637
+ includeCard: true,
638
+ includeVerificationWords: true
639
+ })]);
640
+ console.log(`\nAgent '${data.name}' created \u2014 reachable at ${data.handle}`);
641
+ console.log("E2EE keys generated and stored locally.");
642
+ if (data.verification_words) console.log(`Verification words: ${data.verification_words}`);
527
643
  }
528
- });
529
- agent.command("get").description("Get agent details").argument("<agent-id>", "Agent ID").action(async (agentId) => {
530
- try {
531
- const gOpts = program.opts();
532
- const { client } = await createClient(gOpts.profile);
533
- const data = await client.get(`/agents/${agentId}`);
534
- if (gOpts.json) printJson(data);
535
- else printTable([toAgentRow(data, { includeRevoked: true })]);
536
- } catch (err) {
537
- printError(formatError(err));
538
- process.exitCode = 1;
644
+ }));
645
+ agent.command("list").description("List all agents").option("--include-revoked", "Include revoked agents").action(withClient(program, async ({ client, gOpts }, opts) => {
646
+ const params = opts.includeRevoked ? { include_revoked: true } : void 0;
647
+ const data = await client.get("/agents", params);
648
+ if (gOpts.json) printJson(data);
649
+ else printTable(data.items.map((a) => toAgentRow(a, { includeRevoked: opts.includeRevoked })));
650
+ }));
651
+ agent.command("get").description("Get agent details").argument("<agent-id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, agentId) => {
652
+ const data = await client.get(`/agents/${agentId}`);
653
+ if (gOpts.json) printJson(data);
654
+ else printTable([{
655
+ ...toAgentRow(data, { includeRevoked: true }),
656
+ "Incoming Policy": data.incoming_policy ?? "accept_all",
657
+ "Outgoing Policy": data.outgoing_policy ?? "send_all"
658
+ }]);
659
+ }));
660
+ agent.command("update").description("Update agent details").argument("<agent-id>", "Agent ID").option("--name <name>", "Agent name").option("--[no-]human-oversight", "Require human oversight").option("--incoming-policy <policy>", "Incoming message policy (accept_all|groups_only)").option("--outgoing-policy <policy>", "Outgoing message policy (send_all|groups_only)").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
661
+ const body = {};
662
+ if (opts.name) body.name = opts.name;
663
+ if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight;
664
+ if (opts.incomingPolicy) body.incoming_policy = opts.incomingPolicy;
665
+ if (opts.outgoingPolicy) body.outgoing_policy = opts.outgoingPolicy;
666
+ if (Object.keys(body).length === 0) {
667
+ printError("At least one of --name, --human-oversight, --incoming-policy, --outgoing-policy is required");
668
+ process.exitCode = 2;
669
+ return;
539
670
  }
540
- });
541
- agent.command("update").description("Update agent details").argument("<agent-id>", "Agent ID").option("--name <name>", "Agent name").option("--human-oversight <bool>", "Require human oversight (true/false)").action(async (agentId, opts) => {
542
- try {
543
- const gOpts = program.opts();
544
- const body = {};
545
- if (opts.name) body.name = opts.name;
546
- if (opts.humanOversight !== void 0) body.human_oversight = opts.humanOversight === "true";
547
- if (Object.keys(body).length === 0) {
548
- printError("At least one of --name, --human-oversight is required");
549
- process.exitCode = 2;
550
- return;
551
- }
552
- const { client } = await createClient(gOpts.profile);
553
- await client.patch(`/agents/${agentId}`, body);
671
+ const data = await client.patch(`/agents/${agentId}`, body);
672
+ if (gOpts.json) printJson(data);
673
+ else {
554
674
  printMutationOk("Agent updated", gOpts.json);
555
- } catch (err) {
556
- printError(formatError(err));
557
- process.exitCode = 1;
675
+ if (data.warnings && data.warnings.length > 0) for (const w of data.warnings) process.stderr.write(`Warning: ${w}\n`);
676
+ }
677
+ }));
678
+ agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, agentId, opts) => {
679
+ if (!opts.yes && !gOpts.json) {
680
+ if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
558
681
  }
559
- });
560
- agent.command("revoke").description("Revoke an agent").argument("<agent-id>", "Agent ID").option("--yes", "Skip confirmation prompt").action(async (agentId, opts) => {
561
682
  try {
562
- const gOpts = program.opts();
563
- if (!opts.yes && !gOpts.json) {
564
- if (!await promptConfirm(`Revoke agent ${agentId}?`)) return;
565
- }
566
- const { client } = await createClient(gOpts.profile);
567
683
  await client.delete(`/agents/${agentId}`);
568
684
  printMutationOk("Agent revoked", gOpts.json);
569
685
  } catch (err) {
570
686
  if (err instanceof RineApiError && err.status === 409) printError("Agent already revoked");
571
- else printError(formatError(err));
687
+ else throw err;
572
688
  process.exitCode = 1;
573
689
  }
574
- });
690
+ }));
575
691
  }
576
692
  //#endregion
577
693
  //#region src/commands/auth.ts
@@ -581,126 +697,136 @@ function registerAuth(program) {
581
697
  const profile = program.opts().profile ?? "default";
582
698
  const clientId = opts.clientId ?? await promptText("Client ID: ");
583
699
  const clientSecret = opts.clientSecret ?? await promptPassword("Client secret: ");
584
- const apiUrl = getApiUrl();
585
- const res = await fetch(`${apiUrl}/oauth/token`, {
586
- method: "POST",
587
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
588
- body: new URLSearchParams({
589
- grant_type: "client_credentials",
590
- client_id: clientId,
591
- client_secret: clientSecret
592
- }).toString()
593
- });
594
- if (!res.ok) {
595
- printError(`Login failed: ${(await res.json().catch(() => ({}))).detail ?? res.statusText}`);
596
- process.exitCode = 1;
597
- return;
598
- }
599
- const tokenData = await res.json();
700
+ const tokenData = await fetchOAuthToken(clientId, clientSecret);
600
701
  const creds = loadCredentials();
601
702
  creds[profile] = {
602
703
  client_id: clientId,
603
704
  client_secret: clientSecret
604
705
  };
605
706
  saveCredentials(creds);
606
- const cache = loadTokenCache();
607
- cache[profile] = {
608
- access_token: tokenData.access_token,
609
- expires_at: Date.now() / 1e3 + tokenData.expires_in
610
- };
611
- saveTokenCache(cache);
707
+ cacheToken(profile, tokenData);
612
708
  printSuccess(`Logged in as ${clientId}`);
613
709
  } catch (err) {
614
710
  printError(formatError(err));
615
711
  process.exitCode = 1;
616
712
  }
617
713
  });
618
- program.command("logout").description("Clear token cache for the current profile").action(() => {
619
- try {
620
- const profile = program.opts().profile ?? "default";
621
- const cache = loadTokenCache();
622
- delete cache[profile];
623
- saveTokenCache(cache);
624
- printSuccess("Logged out (credentials preserved — use 'rine auth token' to re-authenticate)");
625
- } catch (err) {
626
- printError(formatError(err));
627
- process.exitCode = 1;
628
- }
629
- });
630
- program.command("status").description("Show org status").action(async () => {
631
- await showOrgStatus(program);
632
- });
714
+ program.command("logout").description("Clear cached token. Credentials are preserved.").action(withErrorHandler(program, (gOpts) => {
715
+ const profile = gOpts.profile ?? "default";
716
+ const cache = loadTokenCache();
717
+ delete cache[profile];
718
+ saveTokenCache(cache);
719
+ printSuccess(`Logged out — token cache cleared for profile '${profile}'. Credentials preserved.`);
720
+ }));
721
+ program.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
722
+ await showOrgStatus(client, gOpts);
723
+ }));
633
724
  const authGroup = program.command("auth").description("Auth commands");
634
- authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(async (opts) => {
635
- try {
636
- const gOpts = program.opts();
637
- const profile = gOpts.profile ?? "default";
638
- const force = opts.force === true;
639
- const now = Date.now() / 1e3;
640
- const envToken = process.env.RINE_TOKEN;
641
- const cached = loadTokenCache()[profile];
642
- const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
643
- const token = await getOrRefreshToken(getCredentialEntry(profile), profile, force);
644
- if (gOpts.json) printResult({
645
- access_token: token,
646
- source
647
- }, gOpts);
648
- else console.log(token);
649
- } catch (err) {
650
- printError(formatError(err));
651
- process.exitCode = 1;
725
+ authGroup.command("token").description("Get or refresh access token").option("--force", "Force token refresh (bypass cache)").action(withErrorHandler(program, async (gOpts, opts) => {
726
+ const profile = gOpts.profile ?? "default";
727
+ const force = opts.force === true;
728
+ const now = Date.now() / 1e3;
729
+ const envToken = process.env.RINE_TOKEN;
730
+ const cached = loadTokenCache()[profile];
731
+ const source = envToken !== void 0 || !force && cached !== void 0 && cached.expires_at - now > 60 ? "cache" : "server";
732
+ const token = await getOrRefreshToken(getCredentialEntry(profile), profile, force);
733
+ if (gOpts.json) printJson({
734
+ access_token: token,
735
+ source
736
+ });
737
+ else console.log(token);
738
+ }));
739
+ authGroup.command("status").description("Show org status").action(withClient(program, async ({ client, gOpts }) => {
740
+ await showOrgStatus(client, gOpts);
741
+ }));
742
+ }
743
+ async function showOrgStatus(client, gOpts) {
744
+ const data = await client.get("/org");
745
+ if (gOpts.json) {
746
+ printJson(data);
747
+ return;
748
+ }
749
+ printTable([
750
+ {
751
+ Field: "Name",
752
+ Value: data.name
753
+ },
754
+ {
755
+ Field: "Slug",
756
+ Value: data.slug
757
+ },
758
+ {
759
+ Field: "Trust Tier",
760
+ Value: String(data.trust_tier)
761
+ },
762
+ {
763
+ Field: "Agent Count",
764
+ Value: String(data.agent_count)
652
765
  }
653
- });
654
- authGroup.command("status").description("Show org status").action(async () => {
655
- await showOrgStatus(program);
656
- });
766
+ ]);
657
767
  }
658
- async function showOrgStatus(program) {
768
+ //#endregion
769
+ //#region src/resolve-handle.ts
770
+ const UUID_ALIAS_RE = /\/agents\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
771
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
772
+ /**
773
+ * Resolves a handle (e.g. "agent@org.rine.network") to a UUID
774
+ * via WebFinger (RFC 7033).
775
+ *
776
+ * Returns the UUID string on success, or the original input unchanged
777
+ * on failure (caller validates UUID format and reports the error).
778
+ */
779
+ async function resolveHandleViaWebFinger(handle) {
659
780
  try {
660
- const gOpts = program.opts();
661
- const { client } = await createClient(gOpts.profile);
662
- const data = await client.get("/org");
781
+ const params = new URLSearchParams({ resource: `acct:${handle}` });
782
+ const data = await HttpClient.publicGet("/.well-known/webfinger", params);
783
+ for (const alias of data.aliases ?? []) {
784
+ const match = UUID_ALIAS_RE.exec(alias);
785
+ if (match?.[1]) return match[1];
786
+ }
787
+ } catch {}
788
+ return handle;
789
+ }
790
+ //#endregion
791
+ //#region src/commands/discover-groups.ts
792
+ function registerDiscoverGroups(discover, program) {
793
+ discover.command("groups").description("List public groups in the directory").option("-q, --query <query>", "Search query").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").action(withErrorHandler(program, async (gOpts, opts) => {
794
+ const params = new URLSearchParams();
795
+ if (opts.query) params.append("q", opts.query);
796
+ if (opts.limit) params.append("limit", opts.limit);
797
+ if (opts.cursor) params.append("cursor", opts.cursor);
798
+ const data = await HttpClient.publicGet("/directory/groups", params.toString() ? params : void 0);
663
799
  if (gOpts.json) {
664
- printResult(data, gOpts);
800
+ printJson(data);
665
801
  return;
666
802
  }
667
- printTable([
668
- {
669
- Field: "Name",
670
- Value: String(data.name ?? "")
671
- },
672
- {
673
- Field: "Slug",
674
- Value: String(data.slug ?? "")
675
- },
676
- {
677
- Field: "Trust Tier",
678
- Value: String(data.trust_tier ?? "")
679
- },
680
- {
681
- Field: "Agent Count",
682
- Value: String(data.agent_count ?? "")
683
- }
684
- ]);
685
- } catch (err) {
686
- printError(formatError(err));
687
- process.exitCode = 1;
688
- }
803
+ printTable((data.items ?? []).map((g) => ({
804
+ Name: g.name,
805
+ Handle: g.handle,
806
+ Description: g.description ?? "",
807
+ Enrollment: g.enrollment_policy,
808
+ Members: g.member_count
809
+ })));
810
+ })).command("inspect").description("Inspect a public group").argument("<group-id>", "Group ID").action(withErrorHandler(program, async (gOpts, groupId) => {
811
+ const data = await HttpClient.publicGet(`/directory/groups/${groupId}`);
812
+ if (gOpts.json) {
813
+ printJson(data);
814
+ return;
815
+ }
816
+ printTable([{
817
+ ID: data.id,
818
+ Name: data.name,
819
+ Handle: data.handle,
820
+ Description: data.description ?? "",
821
+ "Org ID": data.org_id,
822
+ Enrollment: data.enrollment_policy,
823
+ Members: data.member_count,
824
+ Created: data.created_at
825
+ }]);
826
+ }));
689
827
  }
690
828
  //#endregion
691
829
  //#region src/commands/discover.ts
692
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
693
- async function directoryFetch(path, params) {
694
- const base = getApiUrl();
695
- const qs = params?.toString();
696
- const url = qs ? `${base}${path}?${qs}` : `${base}${path}`;
697
- const res = await fetch(url, { headers: { Accept: "application/json" } });
698
- if (!res.ok) {
699
- const body = await res.text().catch(() => res.statusText);
700
- throw new RineApiError(res.status, body, res);
701
- }
702
- return res.json();
703
- }
704
830
  function trunc(s, len) {
705
831
  const str = String(s ?? "");
706
832
  return str.length > len ? `${str.slice(0, len - 1)}\u2026` : str;
@@ -720,7 +846,7 @@ async function doAgentSearch(opts, gOpts) {
720
846
  if (opts.limit) params.append("limit", opts.limit);
721
847
  if (opts.cursor) params.append("cursor", opts.cursor);
722
848
  if (opts.sort) params.append("sort", opts.sort);
723
- const data = await directoryFetch("/directory/agents", params);
849
+ const data = await HttpClient.publicGet("/directory/agents", params);
724
850
  if (gOpts.json) {
725
851
  printJson(data);
726
852
  return;
@@ -736,132 +862,851 @@ async function doAgentSearch(opts, gOpts) {
736
862
  if (result.next_cursor) printText(`Next cursor: ${result.next_cursor}`);
737
863
  }
738
864
  const collect = (v, prev) => [...prev, v];
739
- function addSearchOptions(cmd, requireQuery) {
865
+ function addAgentSearchCommand(parent, name, description, program, requireQuery) {
866
+ const cmd = parent.command(name).description(description);
740
867
  if (requireQuery) cmd.requiredOption("-q, --query <query>", "Search query");
741
868
  else cmd.option("-q, --query <query>", "Search query");
742
- return cmd.option("--category <cat>", "Category filter (repeatable)", collect, []).option("--tag <tag>", "Tag filter (repeatable)", collect, []).option("--jurisdiction <j>", "Jurisdiction filter").option("--language <lang>", "Language filter (repeatable)", collect, []).option("--verified", "Only verified agents").option("--pricing-model <model>", "Pricing model filter").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").option("--sort <sort>", "Sort order (relevance|name|created_at)");
869
+ cmd.option("--category <cat>", "Category filter (repeatable)", collect, []).option("--tag <tag>", "Tag filter (repeatable)", collect, []).option("--jurisdiction <j>", "Jurisdiction filter").option("--language <lang>", "Language filter (repeatable)", collect, []).option("--verified", "Only verified agents").option("--no-verified", "Only unverified agents").option("--pricing-model <model>", "Pricing model filter").option("--limit <n>", "Max results").option("--cursor <cursor>", "Pagination cursor").option("--sort <sort>", "Sort order (relevance|name|created_at)").action(withErrorHandler(program, async (gOpts, opts) => {
870
+ await doAgentSearch(opts, gOpts);
871
+ }));
743
872
  }
744
873
  function registerDiscover(program) {
745
- const discover = program.command("discover").description("Discover agents in the directory");
746
- addSearchOptions(discover.command("agents").description("List agents in the directory"), false).action(async (opts) => {
747
- const gOpts = program.opts();
748
- try {
749
- await doAgentSearch(opts, gOpts);
750
- } catch (err) {
751
- printError(formatError(err));
752
- process.exitCode = 1;
874
+ const discover = program.command("discover").description("Discover agents and groups in the directory");
875
+ addAgentSearchCommand(discover, "agents", "List agents in the directory", program, false);
876
+ addAgentSearchCommand(discover, "search", "Search agents (--query required)", program, true);
877
+ discover.command("categories").description("List directory categories with counts").action(withErrorHandler(program, async (gOpts) => {
878
+ const data = await HttpClient.publicGet("/directory/categories");
879
+ if (gOpts.json) {
880
+ printJson(data);
881
+ return;
753
882
  }
754
- });
755
- addSearchOptions(discover.command("search").description("Search agents (--query required)"), true).action(async (opts) => {
756
- const gOpts = program.opts();
757
- try {
758
- await doAgentSearch(opts, gOpts);
759
- } catch (err) {
760
- printError(formatError(err));
883
+ printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
884
+ Category: c.name,
885
+ Count: c.count
886
+ })));
887
+ }));
888
+ discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(withErrorHandler(program, async (gOpts, agentIdArg) => {
889
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
890
+ let resolvedId = agentIdArg;
891
+ if (agentIdArg.includes("@")) resolvedId = await resolveHandleViaWebFinger(agentIdArg);
892
+ if (!UUID_RE.test(resolvedId)) {
893
+ printError(`Could not resolve '${agentIdArg}' to an agent UUID. Provide a UUID or a valid handle (e.g. agent@org.rine.network).`);
761
894
  process.exitCode = 1;
895
+ return;
762
896
  }
763
- });
764
- discover.command("categories").description("List directory categories with counts").action(async () => {
765
- const gOpts = program.opts();
766
- try {
767
- const data = await directoryFetch("/directory/categories");
768
- if (gOpts.json) {
769
- printJson(data);
770
- return;
771
- }
772
- printTable((data.items ?? []).sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
773
- Category: c.name,
774
- Count: c.count
775
- })));
776
- } catch (err) {
777
- printError(formatError(err));
778
- process.exitCode = 1;
897
+ const data = await HttpClient.publicGet(`/directory/agents/${resolvedId}`);
898
+ if (gOpts.json) {
899
+ printJson(data);
900
+ return;
779
901
  }
780
- });
781
- discover.command("inspect").description("Inspect a specific agent in the directory").argument("<agent-id>", "Agent UUID or handle").action(async (agentId) => {
782
- const gOpts = program.opts();
783
- if (agentId.includes("@")) {
784
- const match = ((await directoryFetch("/directory/agents", new URLSearchParams({
785
- q: agentId,
786
- limit: "5"
787
- }))).items ?? []).find((i) => i.handle === agentId);
788
- if (!match?.id) {
789
- printError(`No agent found with handle: ${agentId}`);
790
- process.exitCode = 1;
791
- return;
792
- }
793
- agentId = match.id;
902
+ const result = data;
903
+ const card = result.card ?? data;
904
+ const meta = result.directory_metadata ?? {};
905
+ printTable([{
906
+ Name: String(card.name ?? ""),
907
+ Description: trunc(card.description, 120),
908
+ Categories: fmtList(card.rine?.categories ?? card.categories),
909
+ Tags: fmtList(card.rine?.tags ?? card.tags),
910
+ Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
911
+ "Registered At": String(meta.registered_at ?? ""),
912
+ "Messages (30d)": String(meta.messages_processed_30d ?? ""),
913
+ "Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
914
+ }]);
915
+ }));
916
+ registerDiscoverGroups(discover, program);
917
+ }
918
+ //#endregion
919
+ //#region src/resolve-agent.ts
920
+ let cached;
921
+ /**
922
+ * Resolve which agent ID to use.
923
+ * Priority: explicit --agent flag > --as global flag > auto-resolve (single-agent shortcut).
924
+ */
925
+ async function resolveAgent(client, explicit, asFlag) {
926
+ if (explicit) return explicit;
927
+ if (asFlag) return asFlag;
928
+ if (cached === void 0) cached = (await client.get("/agents")).items;
929
+ if (cached.length === 1) {
930
+ const agent = cached[0];
931
+ if (agent) return agent.id;
932
+ }
933
+ if (cached.length === 0) throw new Error("No active agents. Create one with 'rine agent create --name <name>'");
934
+ const lines = cached.map((a) => ` ${a.id} ${a.handle}`).join("\n");
935
+ throw new Error(`Multiple agents found. Specify with --agent <uuid>:\n${lines}`);
936
+ }
937
+ //#endregion
938
+ //#region src/commands/group.ts
939
+ function groupRow(g) {
940
+ return {
941
+ ID: g.id,
942
+ Name: g.name,
943
+ Handle: g.handle,
944
+ Enrollment: g.enrollment_policy,
945
+ Visibility: g.visibility,
946
+ Members: g.member_count,
947
+ Created: g.created_at
948
+ };
949
+ }
950
+ function groupDetailRow(g) {
951
+ return {
952
+ ID: g.id,
953
+ Name: g.name,
954
+ Handle: g.handle,
955
+ Description: g.description ?? "",
956
+ Enrollment: g.enrollment_policy,
957
+ Visibility: g.visibility,
958
+ Isolated: g.isolated,
959
+ "Vote Duration": g.vote_duration_hours,
960
+ Members: g.member_count,
961
+ Created: g.created_at
962
+ };
963
+ }
964
+ function memberRow(m) {
965
+ return {
966
+ "Agent ID": m.agent_id,
967
+ Handle: m.agent_handle ?? "",
968
+ Role: m.role,
969
+ Joined: m.joined_at
970
+ };
971
+ }
972
+ function requestRow(r) {
973
+ return {
974
+ "Request ID": r.id,
975
+ "Agent ID": r.agent_id,
976
+ Message: r.message ?? "",
977
+ "Your Vote": r.your_vote ?? "",
978
+ Expires: r.expires_at ?? "",
979
+ Created: r.created_at
980
+ };
981
+ }
982
+ function registerGroup(program) {
983
+ const group = program.command("group").description("Group management");
984
+ group.command("create").description("Create a new group").requiredOption("--name <name>", "Group name (DNS-safe slug)").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--isolated", "Isolate group communication").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, opts) => {
985
+ const body = { name: opts.name };
986
+ if (opts.enrollment) body.enrollment_policy = opts.enrollment;
987
+ if (opts.visibility) body.visibility = opts.visibility;
988
+ else {
989
+ body.visibility = opts.enrollment === "open" ? "public" : "private";
990
+ if (!gOpts.json) printText(`Visibility defaulted to "${body.visibility}" (enrollment: ${opts.enrollment ?? "closed"})`);
794
991
  }
795
- if (!UUID_RE.test(agentId)) {
796
- printError("agent-id must be a valid UUID");
992
+ if (opts.isolated) body.isolated = true;
993
+ if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
994
+ const data = await client.post("/groups", body);
995
+ if (gOpts.json) printJson(data);
996
+ else {
997
+ printTable([groupDetailRow(data)]);
998
+ printText(`\nGroup created — handle: ${data.handle}`);
999
+ }
1000
+ }));
1001
+ group.command("list").description("List groups").action(withClient(program, async ({ client, gOpts }) => {
1002
+ const data = await client.get("/groups");
1003
+ if (gOpts.json) printJson(data);
1004
+ else printTable(data.items.map(groupRow));
1005
+ }));
1006
+ group.command("get").description("Get group details").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1007
+ const data = await client.get(`/groups/${groupId}`);
1008
+ if (gOpts.json) printJson(data);
1009
+ else printTable([groupDetailRow(data)]);
1010
+ }));
1011
+ group.command("update").description("Update group settings").argument("<group-id>", "Group ID").option("--description <desc>", "Group description").option("--enrollment <policy>", "Enrollment policy (open|closed|majority|unanimity)").option("--visibility <vis>", "Visibility (public|private)").option("--vote-duration <hours>", "Vote duration in hours (1-72)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1012
+ const body = {};
1013
+ if (opts.description !== void 0) body.description = opts.description;
1014
+ if (opts.enrollment) body.enrollment_policy = opts.enrollment;
1015
+ if (opts.visibility) body.visibility = opts.visibility;
1016
+ if (opts.voteDuration) body.vote_duration_hours = Number(opts.voteDuration);
1017
+ if (Object.keys(body).length === 0) {
1018
+ printError("At least one of --description, --enrollment, --visibility, --vote-duration is required");
797
1019
  process.exitCode = 2;
798
1020
  return;
799
1021
  }
800
- try {
801
- const data = await directoryFetch(`/directory/agents/${agentId}`);
802
- if (gOpts.json) {
803
- printJson(data);
804
- return;
805
- }
806
- const result = data;
807
- const card = result.card ?? data;
808
- const meta = result.directory_metadata ?? {};
809
- printTable([{
810
- Name: String(card.name ?? ""),
811
- Description: trunc(card.description, 120),
812
- Categories: fmtList(card.rine?.categories ?? card.categories),
813
- Tags: fmtList(card.rine?.tags ?? card.tags),
814
- Pricing: String(card.rine?.pricing_model ?? card.pricing_model ?? ""),
815
- "Registered At": String(meta.registered_at ?? ""),
816
- "Messages (30d)": String(meta.messages_processed_30d ?? ""),
817
- "Avg Response (ms)": String(meta.avg_response_time_ms ?? "")
818
- }]);
819
- } catch (err) {
820
- printError(formatError(err));
821
- process.exitCode = 1;
1022
+ const data = await client.patch(`/groups/${groupId}`, body);
1023
+ if (gOpts.json) printJson(data);
1024
+ else {
1025
+ printTable([groupDetailRow(data)]);
1026
+ printText("Group updated");
822
1027
  }
1028
+ }));
1029
+ group.command("delete").description("Delete a group").argument("<group-id>", "Group ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1030
+ if (!opts.yes && !gOpts.json) {
1031
+ if (!await promptConfirm(`Delete group ${groupId}?`)) return;
1032
+ }
1033
+ await client.delete(`/groups/${groupId}`);
1034
+ printMutationOk("Group deleted", gOpts.json);
1035
+ }));
1036
+ group.command("members").description("List group members").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1037
+ const data = await client.get(`/groups/${groupId}/members`);
1038
+ if (gOpts.json) printJson(data);
1039
+ else printTable(data.items.map(memberRow));
1040
+ }));
1041
+ group.command("join").description("Join a group").argument("<group-id>", "Group ID").option("--message <msg>", "Join request message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1042
+ const body = {};
1043
+ if (opts.message) body.message = opts.message;
1044
+ const data = await client.post(`/groups/${groupId}/join`, body);
1045
+ if (gOpts.json) printJson(data);
1046
+ else if ("role" in data) {
1047
+ printTable([memberRow(data)]);
1048
+ printText("Joined group (member)");
1049
+ } else {
1050
+ printTable([requestRow(data)]);
1051
+ printText("Join request submitted (pending approval)");
1052
+ }
1053
+ }));
1054
+ group.command("leave").description("Leave a group").argument("<group-id>", "Group ID").option("--agent <id>", "Agent ID (defaults to your agent)").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1055
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1056
+ await client.delete(`/groups/${groupId}/members/${agentId}`);
1057
+ printMutationOk("Left group", gOpts.json);
1058
+ }));
1059
+ group.command("kick").description("Remove a member from the group (admin)").argument("<group-id>", "Group ID").argument("<agent-id>", "Agent ID to remove").action(withClient(program, async ({ client, gOpts }, groupId, agentId) => {
1060
+ await client.delete(`/groups/${groupId}/members/${agentId}`);
1061
+ printMutationOk("Member removed", gOpts.json);
1062
+ }));
1063
+ group.command("invite").description("Invite an agent to the group").argument("<group-id>", "Group ID").requiredOption("--agent <id>", "Agent ID to invite").option("--message <msg>", "Invitation message").action(withClient(program, async ({ client, gOpts }, groupId, opts) => {
1064
+ const body = { agent_id: opts.agent };
1065
+ if (opts.message) body.message = opts.message;
1066
+ const data = await client.post(`/groups/${groupId}/invite`, body);
1067
+ if (gOpts.json) printJson(data);
1068
+ else {
1069
+ printText("Invitation sent");
1070
+ if (data.id) printTable([requestRow(data)]);
1071
+ }
1072
+ }));
1073
+ group.command("requests").description("List pending join requests").argument("<group-id>", "Group ID").action(withClient(program, async ({ client, gOpts }, groupId) => {
1074
+ const data = await client.get(`/groups/${groupId}/requests`);
1075
+ if (gOpts.json) printJson(data);
1076
+ else printTable(data.items.map(requestRow));
1077
+ }));
1078
+ group.command("vote").description("Vote on a join request").argument("<group-id>", "Group ID").argument("<request-id>", "Join request ID").requiredOption("--vote <vote>", "Vote: approve or deny").action(withClient(program, async ({ client, gOpts }, groupId, requestId, opts) => {
1079
+ if (opts.vote !== "approve" && opts.vote !== "deny") {
1080
+ printError("--vote must be 'approve' or 'deny'");
1081
+ process.exitCode = 2;
1082
+ return;
1083
+ }
1084
+ const data = await client.post(`/groups/${groupId}/requests/${requestId}/vote`, { vote: opts.vote });
1085
+ if (gOpts.json) printJson(data);
1086
+ else printText(`Vote recorded. Request status: ${data.status}`);
1087
+ }));
1088
+ }
1089
+ //#endregion
1090
+ //#region src/crypto/envelope.ts
1091
+ const SIGNATURE_SIZE = 64;
1092
+ const MAX_KID_LENGTH = 255;
1093
+ function encodeEnvelope(kid, signature, payload) {
1094
+ const kidBytes = new TextEncoder().encode(kid);
1095
+ if (kidBytes.length > MAX_KID_LENGTH) throw new Error(`kid too long: ${kidBytes.length} bytes (max ${MAX_KID_LENGTH})`);
1096
+ if (signature.length !== SIGNATURE_SIZE) throw new Error(`signature must be ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
1097
+ const buf = new Uint8Array(1 + kidBytes.length + SIGNATURE_SIZE + payload.length);
1098
+ buf[0] = kidBytes.length;
1099
+ buf.set(kidBytes, 1);
1100
+ buf.set(signature, 1 + kidBytes.length);
1101
+ buf.set(payload, 1 + kidBytes.length + SIGNATURE_SIZE);
1102
+ return buf;
1103
+ }
1104
+ function decodeEnvelope(data) {
1105
+ if (data.length < 1 + SIGNATURE_SIZE) throw new Error("envelope too short");
1106
+ const kidLen = data[0];
1107
+ const minLen = 1 + kidLen + SIGNATURE_SIZE;
1108
+ if (data.length < minLen) throw new Error("envelope too short for declared kid length");
1109
+ return {
1110
+ kid: new TextDecoder("utf-8", { fatal: true }).decode(data.subarray(1, 1 + kidLen)),
1111
+ signature: data.slice(1 + kidLen, 1 + kidLen + SIGNATURE_SIZE),
1112
+ payload: data.slice(1 + kidLen + SIGNATURE_SIZE)
1113
+ };
1114
+ }
1115
+ //#endregion
1116
+ //#region src/crypto/hpke.ts
1117
+ const VERSION_HPKE = 1;
1118
+ const ENC_SIZE = 32;
1119
+ const INFO = new TextEncoder().encode("rine.e2ee.v1.hpke");
1120
+ const suite = new CipherSuite({
1121
+ kem: new DhkemX25519HkdfSha256(),
1122
+ kdf: new HkdfSha256(),
1123
+ aead: new Aes256Gcm()
1124
+ });
1125
+ async function seal(recipientPublicKey, innerEnvelope, aad) {
1126
+ const pk = await suite.kem.deserializePublicKey(recipientPublicKey);
1127
+ const sender = await suite.createSenderContext({
1128
+ recipientPublicKey: pk,
1129
+ info: INFO
1130
+ });
1131
+ const ciphertext = new Uint8Array(await sender.seal(innerEnvelope, aad));
1132
+ const enc = new Uint8Array(sender.enc);
1133
+ const out = new Uint8Array(1 + ENC_SIZE + ciphertext.length);
1134
+ out[0] = VERSION_HPKE;
1135
+ out.set(enc, 1);
1136
+ out.set(ciphertext, 1 + ENC_SIZE);
1137
+ return out;
1138
+ }
1139
+ async function open(recipientPrivateKey, encryptedPayload, aad) {
1140
+ if (encryptedPayload.length < 1 + ENC_SIZE + 1) throw new Error("encrypted payload too short");
1141
+ const version = encryptedPayload[0];
1142
+ if (version !== VERSION_HPKE) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
1143
+ const enc = encryptedPayload.slice(1, 1 + ENC_SIZE);
1144
+ const ct = encryptedPayload.slice(1 + ENC_SIZE);
1145
+ const sk = await suite.kem.deserializePrivateKey(recipientPrivateKey);
1146
+ const recipient = await suite.createRecipientContext({
1147
+ recipientKey: sk,
1148
+ enc,
1149
+ info: INFO
823
1150
  });
1151
+ return new Uint8Array(await recipient.open(ct, aad));
1152
+ }
1153
+ //#endregion
1154
+ //#region src/crypto/sender-keys-helpers.ts
1155
+ function uuidToBytes(uuid) {
1156
+ const matches = uuid.replace(/-/g, "").match(/../g);
1157
+ if (!matches) throw new Error(`Invalid UUID: ${uuid}`);
1158
+ return new Uint8Array(matches.map((b) => Number.parseInt(b, 16)));
1159
+ }
1160
+ function bytesToUuid(bytes) {
1161
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1162
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1163
+ }
1164
+ function senderKeysDir(agentId) {
1165
+ validatePathId(agentId, "agent ID");
1166
+ return join(getConfigDir(), "keys", agentId, "sender_keys");
1167
+ }
1168
+ function saveSenderKeyState(agentId, groupId, states) {
1169
+ validatePathId(groupId, "group ID");
1170
+ const dir = senderKeysDir(agentId);
1171
+ fs.mkdirSync(dir, {
1172
+ recursive: true,
1173
+ mode: 448
1174
+ });
1175
+ const filePath = join(dir, `${groupId}.json`);
1176
+ const data = states.map((s) => ({
1177
+ ...s,
1178
+ chainKey: toBase64Url(s.chainKey)
1179
+ }));
1180
+ fs.writeFileSync(filePath, JSON.stringify(data), "utf-8");
1181
+ fs.chmodSync(filePath, 384);
1182
+ }
1183
+ function loadSenderKeyStates(agentId, groupId) {
1184
+ validatePathId(groupId, "group ID");
1185
+ const filePath = join(senderKeysDir(agentId), `${groupId}.json`);
1186
+ if (!fs.existsSync(filePath)) return [];
1187
+ const raw = fs.readFileSync(filePath, "utf-8");
1188
+ return JSON.parse(raw).map((s) => ({
1189
+ ...s,
1190
+ chainKey: fromBase64Url(s.chainKey)
1191
+ }));
1192
+ }
1193
+ //#endregion
1194
+ //#region src/crypto/sign.ts
1195
+ function signPayload(signingPrivateKey, plaintext) {
1196
+ return ed25519.sign(plaintext, signingPrivateKey);
1197
+ }
1198
+ function verifySignature(signingPublicKey, plaintext, signature) {
1199
+ try {
1200
+ return ed25519.verify(signature, plaintext, signingPublicKey);
1201
+ } catch {
1202
+ return false;
1203
+ }
1204
+ }
1205
+ //#endregion
1206
+ //#region src/crypto/sender-keys.ts
1207
+ const ROTATION_MESSAGE_LIMIT = 100;
1208
+ const ROTATION_AGE_MS = 10080 * 60 * 1e3;
1209
+ function needsRotation(state) {
1210
+ if (state.messageIndex >= ROTATION_MESSAGE_LIMIT) return true;
1211
+ if (state.createdAt && Date.now() - state.createdAt > ROTATION_AGE_MS) return true;
1212
+ return false;
1213
+ }
1214
+ const VERSION_SENDER_KEY = 2;
1215
+ const SENDER_KEY_ID_SIZE = 16;
1216
+ const MESSAGE_INDEX_SIZE = 4;
1217
+ const NONCE_SIZE = 12;
1218
+ const MAX_SKIP = 1e3;
1219
+ /** Copy Uint8Array into a plain ArrayBuffer-backed Uint8Array (required by Web Crypto). */
1220
+ function toPlainBuffer(src) {
1221
+ const buf = new Uint8Array(src.length);
1222
+ buf.set(src);
1223
+ return buf;
1224
+ }
1225
+ function generateSenderKey(groupId, senderAgentId) {
1226
+ const chainKey = new Uint8Array(32);
1227
+ crypto.getRandomValues(chainKey);
1228
+ return {
1229
+ senderKeyId: crypto.randomUUID(),
1230
+ groupId,
1231
+ senderAgentId,
1232
+ chainKey,
1233
+ messageIndex: 0,
1234
+ createdAt: Date.now()
1235
+ };
1236
+ }
1237
+ function deriveMessageKey(chainKey) {
1238
+ return {
1239
+ messageKey: hmac(sha256, chainKey, new Uint8Array([1])),
1240
+ nextChainKey: hmac(sha256, chainKey, new Uint8Array([2]))
1241
+ };
1242
+ }
1243
+ function advanceChain(state, targetIndex) {
1244
+ if (targetIndex - state.messageIndex > MAX_SKIP) throw new Error(`too many skipped messages: ${targetIndex - state.messageIndex} (max ${MAX_SKIP})`);
1245
+ const skippedKeys = /* @__PURE__ */ new Map();
1246
+ let chainKey = state.chainKey;
1247
+ let index = state.messageIndex;
1248
+ while (index < targetIndex) {
1249
+ const { messageKey, nextChainKey } = deriveMessageKey(chainKey);
1250
+ skippedKeys.set(index, messageKey);
1251
+ chainKey = nextChainKey;
1252
+ index++;
1253
+ }
1254
+ return {
1255
+ updatedState: {
1256
+ ...state,
1257
+ chainKey,
1258
+ messageIndex: index
1259
+ },
1260
+ skippedKeys
1261
+ };
1262
+ }
1263
+ /** Build AAD for sender-key encryption: binds ciphertext to sender_key_id and message_index. */
1264
+ function buildSenderKeyAad(senderKeyId, messageIndex) {
1265
+ const aad = new Uint8Array(SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE);
1266
+ aad.set(senderKeyId, 0);
1267
+ new DataView(aad.buffer).setUint32(SENDER_KEY_ID_SIZE, messageIndex, false);
1268
+ return aad;
1269
+ }
1270
+ async function sealGroup(senderSigningPrivateKey, senderSigningKid, state, plaintext) {
1271
+ const innerEnvelope = encodeEnvelope(senderSigningKid, signPayload(senderSigningPrivateKey, plaintext), plaintext);
1272
+ const { messageKey, nextChainKey } = deriveMessageKey(state.chainKey);
1273
+ const nonce = new Uint8Array(NONCE_SIZE);
1274
+ crypto.getRandomValues(nonce);
1275
+ const idBytes = uuidToBytes(state.senderKeyId);
1276
+ const aad = buildSenderKeyAad(idBytes, state.messageIndex);
1277
+ const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["encrypt"]);
1278
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
1279
+ name: "AES-GCM",
1280
+ iv: nonce,
1281
+ additionalData: toPlainBuffer(aad)
1282
+ }, cryptoKey, toPlainBuffer(innerEnvelope)));
1283
+ messageKey.fill(0);
1284
+ const out = new Uint8Array(1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE + ciphertext.length);
1285
+ let offset = 0;
1286
+ out[offset++] = VERSION_SENDER_KEY;
1287
+ out.set(idBytes, offset);
1288
+ offset += SENDER_KEY_ID_SIZE;
1289
+ new DataView(out.buffer).setUint32(offset, state.messageIndex, false);
1290
+ offset += MESSAGE_INDEX_SIZE;
1291
+ out.set(nonce, offset);
1292
+ offset += NONCE_SIZE;
1293
+ out.set(ciphertext, offset);
1294
+ return {
1295
+ encryptedPayload: out,
1296
+ updatedState: {
1297
+ ...state,
1298
+ chainKey: nextChainKey,
1299
+ messageIndex: state.messageIndex + 1
1300
+ }
1301
+ };
1302
+ }
1303
+ async function openGroup(senderKeyStates, encryptedPayload) {
1304
+ const minLen = 1 + SENDER_KEY_ID_SIZE + MESSAGE_INDEX_SIZE + NONCE_SIZE;
1305
+ if (encryptedPayload.length < minLen) throw new Error("encrypted payload too short");
1306
+ const version = encryptedPayload[0];
1307
+ if (version !== VERSION_SENDER_KEY) throw new Error(`unsupported version: 0x${version?.toString(16).padStart(2, "0")}`);
1308
+ let offset = 1;
1309
+ const senderKeyId = bytesToUuid(encryptedPayload.slice(offset, offset + SENDER_KEY_ID_SIZE));
1310
+ offset += SENDER_KEY_ID_SIZE;
1311
+ const messageIndex = new DataView(encryptedPayload.buffer, encryptedPayload.byteOffset).getUint32(offset, false);
1312
+ offset += MESSAGE_INDEX_SIZE;
1313
+ const nonce = encryptedPayload.slice(offset, offset + NONCE_SIZE);
1314
+ offset += NONCE_SIZE;
1315
+ const ciphertext = encryptedPayload.slice(offset);
1316
+ const state = senderKeyStates.get(senderKeyId);
1317
+ if (!state) throw new Error(`unknown sender key id: ${senderKeyId}`);
1318
+ let messageKey;
1319
+ let updatedState;
1320
+ if (messageIndex < state.messageIndex) {
1321
+ const cached = state.skippedKeys?.[messageIndex];
1322
+ if (!cached) throw new Error("message key expired or already consumed");
1323
+ const { fromBase64Url } = await Promise.resolve().then(() => keys_exports);
1324
+ messageKey = fromBase64Url(cached);
1325
+ const newSkipped = { ...state.skippedKeys };
1326
+ delete newSkipped[messageIndex];
1327
+ updatedState = {
1328
+ ...state,
1329
+ skippedKeys: newSkipped
1330
+ };
1331
+ } else if (messageIndex === state.messageIndex) {
1332
+ const { messageKey: mk, nextChainKey } = deriveMessageKey(state.chainKey);
1333
+ messageKey = mk;
1334
+ updatedState = {
1335
+ ...state,
1336
+ chainKey: nextChainKey,
1337
+ messageIndex: state.messageIndex + 1
1338
+ };
1339
+ } else {
1340
+ const { updatedState: advanced, skippedKeys: newSkipped } = advanceChain(state, messageIndex);
1341
+ const { messageKey: mk, nextChainKey } = deriveMessageKey(advanced.chainKey);
1342
+ messageKey = mk;
1343
+ const mergedSkipped = { ...state.skippedKeys ?? {} };
1344
+ for (const [idx, key] of newSkipped) {
1345
+ mergedSkipped[idx] = toBase64Url(key);
1346
+ key.fill(0);
1347
+ }
1348
+ const entries = Object.entries(mergedSkipped);
1349
+ if (entries.length > MAX_SKIP) {
1350
+ entries.sort((a, b) => Number(a[0]) - Number(b[0]));
1351
+ const excess = entries.length - MAX_SKIP;
1352
+ for (let i = 0; i < excess; i++) delete mergedSkipped[Number(entries[i][0])];
1353
+ }
1354
+ updatedState = {
1355
+ ...advanced,
1356
+ chainKey: nextChainKey,
1357
+ messageIndex: messageIndex + 1,
1358
+ skippedKeys: mergedSkipped
1359
+ };
1360
+ }
1361
+ const aad = buildSenderKeyAad(encryptedPayload.slice(1, 1 + SENDER_KEY_ID_SIZE), messageIndex);
1362
+ const cryptoKey = await crypto.subtle.importKey("raw", toPlainBuffer(messageKey), "AES-GCM", false, ["decrypt"]);
1363
+ const innerEnvelope = new Uint8Array(await crypto.subtle.decrypt({
1364
+ name: "AES-GCM",
1365
+ iv: nonce,
1366
+ additionalData: toPlainBuffer(aad)
1367
+ }, cryptoKey, toPlainBuffer(ciphertext)));
1368
+ messageKey.fill(0);
1369
+ return {
1370
+ innerEnvelope,
1371
+ senderKeyId,
1372
+ messageIndex,
1373
+ updatedState
1374
+ };
824
1375
  }
825
1376
  //#endregion
826
- //#region src/commands/resolve.ts
1377
+ //#region src/crypto/message.ts
1378
+ async function verifyEnvelopeSender(decoded, client) {
1379
+ let verified = false;
1380
+ let verificationStatus = "unverifiable";
1381
+ try {
1382
+ const senderAgentId = agentIdFromKid(decoded.kid);
1383
+ verified = verifySignature(jwkToPublicKey((await client.get(`/agents/${senderAgentId}/keys`)).signing_public_key), decoded.payload, decoded.signature);
1384
+ verificationStatus = verified ? "verified" : "invalid";
1385
+ } catch {}
1386
+ return {
1387
+ verified,
1388
+ verificationStatus
1389
+ };
1390
+ }
1391
+ function signingKid(agentId) {
1392
+ return `rine:${agentId}`;
1393
+ }
1394
+ async function encryptMessage(senderAgentId, recipientEncryptionPk, payload) {
1395
+ const keys = loadAgentKeys(senderAgentId);
1396
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
1397
+ const kid = signingKid(senderAgentId);
1398
+ return {
1399
+ encrypted_payload: toBase64Url(await seal(recipientEncryptionPk, encodeEnvelope(kid, signPayload(keys.signing.privateKey, plaintext), plaintext))),
1400
+ encryption_version: "hpke-v1",
1401
+ sender_signing_kid: kid
1402
+ };
1403
+ }
1404
+ async function fetchRecipientEncryptionKey(client, agentId) {
1405
+ return jwkToPublicKey((await client.get(`/agents/${agentId}/keys`)).encryption_public_key);
1406
+ }
1407
+ async function decryptMessage(recipientAgentId, encryptedPayloadB64, client) {
1408
+ const keys = loadAgentKeys(recipientAgentId);
1409
+ const encrypted = fromBase64Url(encryptedPayloadB64);
1410
+ const decoded = decodeEnvelope(await open(keys.encryption.privateKey, encrypted));
1411
+ const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
1412
+ const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
1413
+ return {
1414
+ plaintext,
1415
+ senderKid: decoded.kid,
1416
+ verified,
1417
+ verificationStatus
1418
+ };
1419
+ }
1420
+ async function encryptGroupMessage(senderAgentId, groupId, senderKeyState, payload) {
1421
+ const keys = loadAgentKeys(senderAgentId);
1422
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
1423
+ const kid = signingKid(senderAgentId);
1424
+ const selfReadKey = deriveMessageKey(senderKeyState.chainKey).messageKey;
1425
+ const selfReadIndex = senderKeyState.messageIndex;
1426
+ const { encryptedPayload, updatedState } = await sealGroup(keys.signing.privateKey, kid, senderKeyState, plaintext);
1427
+ updatedState.skippedKeys = {
1428
+ ...updatedState.skippedKeys ?? {},
1429
+ [selfReadIndex]: toBase64Url(selfReadKey)
1430
+ };
1431
+ selfReadKey.fill(0);
1432
+ saveSenderKeyState(senderAgentId, groupId, [...loadSenderKeyStates(senderAgentId, groupId).filter((s) => s.senderKeyId !== updatedState.senderKeyId), updatedState]);
1433
+ return {
1434
+ result: {
1435
+ encrypted_payload: toBase64Url(encryptedPayload),
1436
+ encryption_version: "sender-key-v1",
1437
+ sender_signing_kid: kid
1438
+ },
1439
+ updatedState
1440
+ };
1441
+ }
1442
+ async function decryptGroupMessage(recipientAgentId, groupId, encryptedPayloadB64, client) {
1443
+ const states = loadSenderKeyStates(recipientAgentId, groupId);
1444
+ const stateMap = /* @__PURE__ */ new Map();
1445
+ for (const s of states) stateMap.set(s.senderKeyId, s);
1446
+ const { innerEnvelope, updatedState, messageIndex } = await openGroup(stateMap, fromBase64Url(encryptedPayloadB64));
1447
+ const decoded = decodeEnvelope(innerEnvelope);
1448
+ const plaintext = new TextDecoder("utf-8", { fatal: true }).decode(decoded.payload);
1449
+ const { verified, verificationStatus } = await verifyEnvelopeSender(decoded, client);
1450
+ if (verificationStatus === "invalid") throw new Error("Sender signature verification failed");
1451
+ if (updatedState.senderAgentId === recipientAgentId) {
1452
+ const prevCached = stateMap.get(updatedState.senderKeyId)?.skippedKeys?.[messageIndex];
1453
+ if (prevCached && !updatedState.skippedKeys?.[messageIndex]) updatedState.skippedKeys = {
1454
+ ...updatedState.skippedKeys ?? {},
1455
+ [messageIndex]: prevCached
1456
+ };
1457
+ }
1458
+ stateMap.set(updatedState.senderKeyId, updatedState);
1459
+ saveSenderKeyState(recipientAgentId, groupId, Array.from(stateMap.values()));
1460
+ return {
1461
+ plaintext,
1462
+ senderKid: decoded.kid,
1463
+ verified,
1464
+ verificationStatus
1465
+ };
1466
+ }
1467
+ function getAgentPublicKeys(agentId, agentKeys) {
1468
+ const keys = agentKeys ?? loadAgentKeys(agentId);
1469
+ return {
1470
+ signing_public_key: signingPublicKeyToJWK(keys.signing.publicKey),
1471
+ encryption_public_key: {
1472
+ kty: "OKP",
1473
+ crv: "X25519",
1474
+ x: toBase64Url(keys.encryption.publicKey)
1475
+ }
1476
+ };
1477
+ }
1478
+ //#endregion
1479
+ //#region src/commands/keys.ts
1480
+ function getKeysDir(agentId) {
1481
+ return join(getConfigDir(), "keys", agentId);
1482
+ }
1483
+ function backupKeys(agentId) {
1484
+ const dir = getKeysDir(agentId);
1485
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1486
+ for (const name of ["signing.key", "encryption.key"]) {
1487
+ const src = join(dir, name);
1488
+ if (!fs.existsSync(src)) continue;
1489
+ for (const f of fs.readdirSync(dir)) if (f.startsWith(`${name}.bak.`)) fs.unlinkSync(join(dir, f));
1490
+ fs.copyFileSync(src, join(dir, `${name}.bak.${ts}`));
1491
+ }
1492
+ }
1493
+ async function verifyServerKeys(client, agentId, expected) {
1494
+ const server = await client.get(`/agents/${agentId}/keys`);
1495
+ if (server.signing_public_key.x !== expected.signing_public_key.x || server.encryption_public_key.x !== expected.encryption_public_key.x) throw new Error("Server key verification failed: uploaded keys do not match. Old keys preserved locally.");
1496
+ }
1497
+ function registerKeys(program) {
1498
+ const keys = program.command("keys").description("E2EE key management");
1499
+ keys.command("generate").description("Generate new E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
1500
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1501
+ if (agentKeysExist(agentId)) {
1502
+ printError(`Keys already exist for agent ${agentId}. Use 'rine keys rotate' to replace them.`);
1503
+ process.exitCode = 1;
1504
+ return;
1505
+ }
1506
+ const agentKeys = generateAgentKeys();
1507
+ const pubKeys = getAgentPublicKeys(agentId, agentKeys);
1508
+ await client.post(`/agents/${agentId}/keys`, pubKeys);
1509
+ await verifyServerKeys(client, agentId, pubKeys);
1510
+ saveAgentKeys(agentId, agentKeys);
1511
+ printMutationOk(`E2EE keys generated for agent ${agentId}`, gOpts.json);
1512
+ }));
1513
+ keys.command("rotate").description("Rotate E2EE keys for an agent").option("--agent <id>", "Agent ID").action(withClient(program, async ({ client, gOpts }, opts) => {
1514
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1515
+ if (agentKeysExist(agentId)) backupKeys(agentId);
1516
+ const agentKeys = generateAgentKeys();
1517
+ const pubKeys = getAgentPublicKeys(agentId, agentKeys);
1518
+ await client.post(`/agents/${agentId}/keys`, pubKeys);
1519
+ await verifyServerKeys(client, agentId, pubKeys);
1520
+ saveAgentKeys(agentId, agentKeys);
1521
+ printMutationOk(`E2EE keys rotated for agent ${agentId}`, gOpts.json);
1522
+ }));
1523
+ keys.command("export").description("Export private keys to a file").option("--agent <id>", "Agent ID").requiredOption("--output <file>", "Output file path").action(withClient(program, async ({ client, gOpts }, opts) => {
1524
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1525
+ if (!agentKeysExist(agentId)) {
1526
+ printError(`No keys found for agent ${agentId}`);
1527
+ process.exitCode = 1;
1528
+ return;
1529
+ }
1530
+ const agentKeys = loadAgentKeys(agentId);
1531
+ const exported = {
1532
+ agent_id: agentId,
1533
+ signing_private_key: toBase64Url(agentKeys.signing.privateKey),
1534
+ encryption_private_key: toBase64Url(agentKeys.encryption.privateKey)
1535
+ };
1536
+ fs.writeFileSync(opts.output, JSON.stringify(exported, null, 2));
1537
+ fs.chmodSync(opts.output, 384);
1538
+ if (gOpts.json) printJson({
1539
+ ok: true,
1540
+ path: opts.output
1541
+ });
1542
+ else console.log(`Keys exported to ${opts.output}`);
1543
+ }));
1544
+ keys.command("import").description("Import private keys from a file").requiredOption("--input <file>", "Input file path").action(withErrorHandler(program, (gOpts, opts) => {
1545
+ const raw = fs.readFileSync(opts.input, "utf-8");
1546
+ const data = JSON.parse(raw);
1547
+ if (!data.agent_id || !data.signing_private_key || !data.encryption_private_key) {
1548
+ printError("Invalid key export file");
1549
+ process.exitCode = 2;
1550
+ return;
1551
+ }
1552
+ const sigBytes = fromBase64Url(data.signing_private_key);
1553
+ if (sigBytes.length !== 32) {
1554
+ printError(`Invalid signing key: expected 32 bytes, got ${sigBytes.length}`);
1555
+ process.exitCode = 2;
1556
+ return;
1557
+ }
1558
+ try {
1559
+ ed25519.getPublicKey(sigBytes);
1560
+ } catch {
1561
+ printError("Invalid signing key: not a valid Ed25519 private key");
1562
+ process.exitCode = 2;
1563
+ return;
1564
+ }
1565
+ const encBytes = fromBase64Url(data.encryption_private_key);
1566
+ if (encBytes.length !== 32) {
1567
+ printError(`Invalid encryption key: expected 32 bytes, got ${encBytes.length}`);
1568
+ process.exitCode = 2;
1569
+ return;
1570
+ }
1571
+ try {
1572
+ x25519.getPublicKey(encBytes);
1573
+ } catch {
1574
+ printError("Invalid encryption key: not a valid X25519 private key");
1575
+ process.exitCode = 2;
1576
+ return;
1577
+ }
1578
+ const dir = join(getConfigDir(), "keys", data.agent_id);
1579
+ fs.mkdirSync(dir, {
1580
+ recursive: true,
1581
+ mode: 448
1582
+ });
1583
+ const sigPath = join(dir, "signing.key");
1584
+ fs.writeFileSync(sigPath, data.signing_private_key, "utf-8");
1585
+ fs.chmodSync(sigPath, 384);
1586
+ const encPath = join(dir, "encryption.key");
1587
+ fs.writeFileSync(encPath, data.encryption_private_key, "utf-8");
1588
+ fs.chmodSync(encPath, 384);
1589
+ printMutationOk(`Keys imported for agent ${data.agent_id}`, gOpts.json);
1590
+ }));
1591
+ }
1592
+ //#endregion
1593
+ //#region src/crypto/ingest.ts
827
1594
  /**
828
- * Resolve the first active agent for the authenticated org.
829
- * Used by inbox and stream commands when no explicit --agent is given.
1595
+ * Auto-ingest a sender key distribution from a decrypted message.
1596
+ * Only ingests if the message is a `rine.v1.sender_key_distribution`,
1597
+ * the signature was verified, and the key ID is not already known.
1598
+ *
1599
+ * Returns true if a new key was ingested.
830
1600
  */
831
- async function resolveFirstAgent(client) {
832
- const agents = ((await client.get("/agents")).items ?? []).filter((a) => !a.revoked_at);
833
- if (agents.length === 0) throw new Error("No active agents found. Create one with `rine agent create`.");
834
- const first = agents[0];
835
- if (agents.length > 1) process.stderr.write(`Warning: multiple agents found, using first: ${first.id}\n`);
836
- return first.id;
1601
+ function ingestSenderKeyDistribution(agentId, messageType, result) {
1602
+ if (messageType !== "rine.v1.sender_key_distribution") return false;
1603
+ if (!result.verified) return false;
1604
+ try {
1605
+ const dist = JSON.parse(result.plaintext);
1606
+ const existing = loadSenderKeyStates(agentId, dist.group_id);
1607
+ if (existing.some((s) => s.senderKeyId === dist.sender_key_id)) return false;
1608
+ const newState = {
1609
+ senderKeyId: dist.sender_key_id,
1610
+ groupId: dist.group_id,
1611
+ senderAgentId: agentIdFromKid(result.senderKid),
1612
+ chainKey: fromBase64Url(dist.chain_key),
1613
+ messageIndex: dist.message_index
1614
+ };
1615
+ saveSenderKeyState(agentId, dist.group_id, [...existing, newState]);
1616
+ return true;
1617
+ } catch {
1618
+ return false;
1619
+ }
1620
+ }
1621
+ //#endregion
1622
+ //#region src/commands/sender-key-ops.ts
1623
+ async function distributeSenderKey(client, senderAgentId, state, groupId, recipientIds, extraHeaders) {
1624
+ if (recipientIds.length === 0) return [];
1625
+ const batchKeys = await client.get("/agents/keys", { ids: recipientIds.join(",") });
1626
+ const distPayload = {
1627
+ group_id: groupId,
1628
+ sender_key_id: state.senderKeyId,
1629
+ chain_key: toBase64Url(state.chainKey),
1630
+ message_index: state.messageIndex,
1631
+ sender_agent_id: senderAgentId
1632
+ };
1633
+ const succeeded = [];
1634
+ const failed = [];
1635
+ for (const recipientId of recipientIds) {
1636
+ const recipientKeyData = batchKeys.keys[recipientId];
1637
+ if (!recipientKeyData) {
1638
+ failed.push(recipientId);
1639
+ continue;
1640
+ }
1641
+ try {
1642
+ const encrypted = await encryptMessage(senderAgentId, fromBase64Url(recipientKeyData.encryption_public_key.x), distPayload);
1643
+ await client.post("/messages", {
1644
+ to_agent_id: recipientId,
1645
+ type: "rine.v1.sender_key_distribution",
1646
+ ...encrypted
1647
+ }, extraHeaders);
1648
+ succeeded.push(recipientId);
1649
+ } catch {
1650
+ failed.push(recipientId);
1651
+ }
1652
+ }
1653
+ if (failed.length > 0) printError(`Sender key distribution failed for ${failed.length} recipient(s): ${failed.join(", ")}`);
1654
+ return succeeded;
1655
+ }
1656
+ async function getOrCreateSenderKey(client, senderAgentId, groupHandle, extraHeaders) {
1657
+ const firstGroup = (await client.get("/groups", { handle: groupHandle })).items?.[0];
1658
+ if (!firstGroup) throw new Error(`Group not found: ${groupHandle}`);
1659
+ const groupId = firstGroup.id;
1660
+ const memberIds = (await client.get(`/groups/${groupId}/members`)).items.map((m) => m.agent_id);
1661
+ const senderIdLower = senderAgentId.toLowerCase();
1662
+ if (!memberIds.some((id) => id.toLowerCase() === senderIdLower)) throw new Error("You are not a member of this group");
1663
+ const recipientIds = memberIds.filter((id) => id.toLowerCase() !== senderIdLower);
1664
+ const states = loadSenderKeyStates(senderAgentId, groupId);
1665
+ const own = states.find((s) => s.senderAgentId === senderAgentId);
1666
+ const currentMemberSet = new Set(memberIds.map((id) => id.toLowerCase()));
1667
+ const memberRemoved = own?.distributedTo?.some((id) => !currentMemberSet.has(id.toLowerCase())) ?? false;
1668
+ if (own && !needsRotation(own) && !memberRemoved) {
1669
+ const alreadyDistributed = new Set((own.distributedTo ?? []).map((id) => id.toLowerCase()));
1670
+ const undistributed = recipientIds.filter((id) => !alreadyDistributed.has(id.toLowerCase()));
1671
+ if (undistributed.length > 0) {
1672
+ const succeeded = await distributeSenderKey(client, senderAgentId, own, groupId, undistributed, extraHeaders);
1673
+ if (succeeded.length > 0) {
1674
+ own.distributedTo = [...own.distributedTo ?? [], ...succeeded];
1675
+ saveSenderKeyState(senderAgentId, groupId, states);
1676
+ }
1677
+ }
1678
+ return {
1679
+ state: own,
1680
+ groupId
1681
+ };
1682
+ }
1683
+ const newState = generateSenderKey(groupId, senderAgentId);
1684
+ newState.distributedTo = await distributeSenderKey(client, senderAgentId, newState, groupId, recipientIds, extraHeaders);
1685
+ saveSenderKeyState(senderAgentId, groupId, [...memberRemoved ? states.filter((s) => currentMemberSet.has(s.senderAgentId.toLowerCase())) : states, newState]);
1686
+ return {
1687
+ state: newState,
1688
+ groupId
1689
+ };
837
1690
  }
838
1691
  //#endregion
839
1692
  //#region src/commands/messages.ts
840
1693
  function msgFrom(m) {
841
- return String(m.sender_handle ?? m.from_agent_id ?? "");
1694
+ return m.sender_handle ?? m.from_agent_id ?? "";
842
1695
  }
843
1696
  function msgTo(m) {
844
- return String(m.recipient_handle ?? m.to_agent_id ?? "");
1697
+ return m.recipient_handle ?? m.to_agent_id ?? "";
845
1698
  }
846
- function sendRow(m) {
847
- return {
848
- ID: m.id,
849
- "Conversation ID": m.conversation_id,
850
- From: msgFrom(m),
851
- To: msgTo(m),
852
- Type: m.type,
853
- Created: m.created_at
854
- };
1699
+ function msgGroup(m) {
1700
+ return m.group_handle ?? "";
855
1701
  }
856
- function readRow(m) {
1702
+ function sendRow(m) {
857
1703
  return {
858
1704
  ID: m.id,
859
1705
  "Conversation ID": m.conversation_id,
860
1706
  From: msgFrom(m),
861
1707
  To: msgTo(m),
1708
+ Group: msgGroup(m),
862
1709
  Type: m.type,
863
- Preview: m.payload_preview ? String(m.payload_preview).slice(0, 60) : "",
864
- Payload: JSON.stringify(m.payload ?? {}),
865
1710
  Created: m.created_at
866
1711
  };
867
1712
  }
@@ -869,98 +1714,143 @@ function inboxRow(m) {
869
1714
  return {
870
1715
  ID: m.id,
871
1716
  From: msgFrom(m),
1717
+ Group: msgGroup(m),
872
1718
  Type: m.type,
873
- Preview: m.payload_preview ? String(m.payload_preview).slice(0, 60) : "",
1719
+ Encryption: m.encryption_version,
874
1720
  Created: m.created_at
875
1721
  };
876
1722
  }
877
1723
  function addMessageCommands(parent, program) {
878
- parent.command("send").description("Send a message").requiredOption("--to <address>", "Recipient address (agent ID or handle with @)").requiredOption("--type <type>", "Message type (e.g. rine.v1.task_request)").requiredOption("--payload <json>", "Message payload as JSON string").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(async (opts) => {
1724
+ parent.command("send").description("Send an encrypted message").requiredOption("--to <address>", "Recipient: agent handle, agent ID, or #group@org handle").requiredOption("--type <type>", "Message type (e.g. rine.v1.task_request)").requiredOption("--payload <json>", "Message payload as JSON string").option("--from <address>", "Sender address (agent ID or handle with @)").option("--idempotency-key <key>", "Idempotency key header").action(withClient(program, async ({ client, gOpts }, opts) => {
1725
+ let parsedPayload;
879
1726
  try {
880
- const gOpts = program.opts();
881
- const { client } = await createClient(gOpts.profile);
882
- let parsedPayload;
883
- try {
884
- parsedPayload = JSON.parse(opts.payload);
885
- } catch {
886
- printError("--payload must be valid JSON");
887
- process.exitCode = 2;
888
- return;
889
- }
890
- const body = {
891
- type: opts.type,
892
- payload: parsedPayload
893
- };
894
- if (opts.to.includes("@")) body.to_handle = opts.to;
895
- else body.to_agent_id = opts.to;
896
- if (opts.from !== void 0) if (opts.from.includes("@")) body.from_handle = opts.from;
897
- else body.from_agent_id = opts.from;
898
- const extraHeaders = {};
899
- if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
900
- const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
901
- if (gOpts.json) printJson(data);
902
- else printTable([sendRow(data)]);
903
- } catch (err) {
904
- printError(formatError(err));
905
- process.exitCode = 1;
1727
+ parsedPayload = JSON.parse(opts.payload);
1728
+ } catch {
1729
+ printError("--payload must be valid JSON");
1730
+ process.exitCode = 2;
1731
+ return;
906
1732
  }
907
- });
908
- parent.command("read").description("Read a message by ID").argument("<message-id>", "Message ID").action(async (messageId) => {
909
- try {
910
- const gOpts = program.opts();
911
- const { client } = await createClient(gOpts.profile);
912
- const data = await client.get(`/messages/${messageId}`);
913
- if (gOpts.json) printJson(data);
914
- else printTable([readRow(data)]);
1733
+ const extraHeaders = {};
1734
+ let senderAgentId;
1735
+ if (opts.from !== void 0) if (opts.from.includes("@")) {
1736
+ const resolved = await resolveHandleViaWebFinger(opts.from);
1737
+ if (!UUID_RE.test(resolved)) throw new Error("Cannot resolve sender handle to agent ID. Ensure WebFinger is available or use a UUID.");
1738
+ senderAgentId = resolved;
1739
+ extraHeaders["X-Rine-Agent"] = resolved;
1740
+ } else {
1741
+ senderAgentId = opts.from;
1742
+ extraHeaders["X-Rine-Agent"] = opts.from;
1743
+ }
1744
+ else if (!gOpts.as) {
1745
+ senderAgentId = await resolveAgent(client, void 0, void 0);
1746
+ extraHeaders["X-Rine-Agent"] = senderAgentId;
1747
+ } else senderAgentId = gOpts.as;
1748
+ if (opts.idempotencyKey) extraHeaders["Idempotency-Key"] = opts.idempotencyKey;
1749
+ let recipientAgentId;
1750
+ if (!opts.to.includes("@")) recipientAgentId = opts.to;
1751
+ else if (!opts.to.startsWith("#")) {
1752
+ const resolved = await resolveHandleViaWebFinger(opts.to);
1753
+ if (!UUID_RE.test(resolved)) throw new Error("Cannot resolve recipient handle to agent ID. Ensure WebFinger is available or use a UUID.");
1754
+ recipientAgentId = resolved;
1755
+ }
1756
+ const body = { type: opts.type };
1757
+ if (opts.to.includes("@")) body.to_handle = opts.to;
1758
+ else body.to_agent_id = opts.to;
1759
+ if (opts.from?.includes("@")) body.from_handle = opts.from;
1760
+ if (opts.to.startsWith("#") && senderAgentId) {
1761
+ const { state, groupId } = await getOrCreateSenderKey(client, senderAgentId, opts.to, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1762
+ const { result } = await encryptGroupMessage(senderAgentId, groupId, state, parsedPayload);
1763
+ Object.assign(body, result);
1764
+ } else if (senderAgentId && recipientAgentId) {
1765
+ const recipientPk = await fetchRecipientEncryptionKey(client, recipientAgentId);
1766
+ const encrypted = await encryptMessage(senderAgentId, recipientPk, parsedPayload);
1767
+ Object.assign(body, encrypted);
1768
+ } else throw new Error("Cannot encrypt: unable to resolve sender or recipient. Use UUIDs or ensure WebFinger is available.");
1769
+ const data = await client.post("/messages", body, Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0);
1770
+ if (gOpts.json) printJson(data);
1771
+ else printTable([sendRow(data)]);
1772
+ }));
1773
+ parent.command("read").description("Read and decrypt a message by ID").argument("<message-id>", "Message ID").option("--agent <id>", "Agent ID (for decryption key)").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
1774
+ const data = await client.get(`/messages/${messageId}`);
1775
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1776
+ if ((data.encryption_version === "hpke-v1" || data.encryption_version === "sender-key-v1") && agentKeysExist(agentId)) try {
1777
+ let result;
1778
+ if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
1779
+ else result = await decryptMessage(agentId, data.encrypted_payload, client);
1780
+ if (ingestSenderKeyDistribution(agentId, data.type, result)) printText("Sender key ingested");
1781
+ if (gOpts.json) printJson({
1782
+ ...data,
1783
+ decrypted_payload: JSON.parse(result.plaintext),
1784
+ signature_verified: result.verified,
1785
+ verification_status: result.verificationStatus,
1786
+ sender_kid: result.senderKid
1787
+ });
1788
+ else printTable([{
1789
+ ID: data.id,
1790
+ "Conversation ID": data.conversation_id,
1791
+ From: msgFrom(data),
1792
+ To: msgTo(data),
1793
+ Group: msgGroup(data),
1794
+ Type: data.type,
1795
+ Verified: result.verificationStatus === "verified" ? "✓" : result.verificationStatus === "invalid" ? "✗ INVALID" : "?",
1796
+ Payload: result.plaintext.slice(0, 200),
1797
+ Created: data.created_at
1798
+ }]);
1799
+ return;
915
1800
  } catch (err) {
916
- printError(formatError(err));
917
- process.exitCode = 1;
1801
+ if (!gOpts.json) printError(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`);
918
1802
  }
919
- });
920
- parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (auto-resolved if omitted)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(async (opts) => {
1803
+ if (gOpts.json) printJson(data);
1804
+ else printTable([{
1805
+ ID: data.id,
1806
+ "Conversation ID": data.conversation_id,
1807
+ From: msgFrom(data),
1808
+ To: msgTo(data),
1809
+ Group: msgGroup(data),
1810
+ Type: data.type,
1811
+ Encryption: data.encryption_version,
1812
+ "Encrypted Payload": `${data.encrypted_payload.slice(0, 40)}...`,
1813
+ Created: data.created_at
1814
+ }]);
1815
+ }));
1816
+ parent.command("inbox").description("List inbox messages").option("--agent <id>", "Agent ID (defaults to your agent)").option("--limit <n>", "Maximum number of messages to return").option("--cursor <cursor>", "Pagination cursor").action(withClient(program, async ({ client, gOpts }, opts) => {
1817
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1818
+ const params = {};
1819
+ if (opts.limit) params.limit = opts.limit;
1820
+ if (opts.cursor) params.cursor = opts.cursor;
1821
+ const data = await client.get(`/agents/${agentId}/messages`, params);
1822
+ if (gOpts.json) printJson(data);
1823
+ else {
1824
+ printTable(data.items.map(inboxRow));
1825
+ if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
1826
+ }
1827
+ }));
1828
+ parent.command("reply").description("Reply to a message (encrypted)").argument("<message-id>", "Message ID to reply to").requiredOption("--type <type>", "Message type").requiredOption("--payload <json>", "Reply payload as JSON string").action(withClient(program, async ({ client, gOpts }, messageId, opts) => {
1829
+ let parsedPayload;
921
1830
  try {
922
- const gOpts = program.opts();
923
- const { client } = await createClient(gOpts.profile);
924
- let agentId = opts.agent;
925
- if (!agentId) agentId = await resolveFirstAgent(client);
926
- const params = {};
927
- if (opts.limit) params.limit = opts.limit;
928
- if (opts.cursor) params.cursor = opts.cursor;
929
- const data = await client.get(`/agents/${agentId}/messages`, params);
930
- if (gOpts.json) printJson(data);
931
- else {
932
- printTable(data.items.map(inboxRow));
933
- if (data.next_cursor) printText(`Next cursor: ${data.next_cursor}`);
934
- }
935
- } catch (err) {
936
- printError(formatError(err));
937
- process.exitCode = 1;
1831
+ parsedPayload = JSON.parse(opts.payload);
1832
+ } catch {
1833
+ printError("--payload must be valid JSON");
1834
+ process.exitCode = 2;
1835
+ return;
938
1836
  }
939
- });
940
- parent.command("reply").description("Reply to a message").argument("<message-id>", "Message ID to reply to").requiredOption("--type <type>", "Message type").requiredOption("--payload <json>", "Reply payload as JSON string").action(async (messageId, opts) => {
1837
+ const original = await client.get(`/messages/${messageId}`);
1838
+ const senderAgentId = await resolveAgent(client, void 0, gOpts.as);
1839
+ const recipientAgentId = original.from_agent_id;
941
1840
  try {
942
- const gOpts = program.opts();
943
- const { client } = await createClient(gOpts.profile);
944
- let parsedPayload;
945
- try {
946
- parsedPayload = JSON.parse(opts.payload);
947
- } catch {
948
- printError("--payload must be valid JSON");
949
- process.exitCode = 2;
950
- return;
951
- }
952
- const data = await client.post(`/messages/${messageId}/reply`, {
953
- type: opts.type,
954
- payload: parsedPayload
955
- });
1841
+ const body = { type: opts.type };
1842
+ if (!recipientAgentId || !agentKeysExist(senderAgentId)) throw new Error("Cannot reply: encryption keys unavailable. Run 'rine keys generate' first.");
1843
+ const encrypted = await encryptMessage(senderAgentId, await fetchRecipientEncryptionKey(client, recipientAgentId), parsedPayload);
1844
+ Object.assign(body, encrypted);
1845
+ const data = await client.post(`/messages/${messageId}/reply`, body);
956
1846
  if (gOpts.json) printJson(data);
957
1847
  else printTable([sendRow(data)]);
958
1848
  } catch (err) {
959
1849
  if (err instanceof RineApiError && err.status === 409) printError("Conversation is closed");
960
- else printError(formatError(err));
1850
+ else throw err;
961
1851
  process.exitCode = 1;
962
1852
  }
963
- });
1853
+ }));
964
1854
  }
965
1855
  function registerMessages(program) {
966
1856
  addMessageCommands(program, program);
@@ -974,45 +1864,31 @@ function renderOrg(data, opts) {
974
1864
  return;
975
1865
  }
976
1866
  printTable([{
977
- ID: String(data.id ?? ""),
978
- Name: String(data.name ?? ""),
979
- "Contact Email": String(data.contact_email ?? ""),
980
- Country: String(data.country_code ?? ""),
981
- Created: String(data.created_at ?? "")
1867
+ ID: data.id,
1868
+ Name: data.name,
1869
+ "Contact Email": data.contact_email ?? "",
1870
+ Country: data.country_code ?? "",
1871
+ Created: data.created_at
982
1872
  }]);
983
1873
  }
984
1874
  function registerOrg(program) {
985
1875
  const org = program.command("org").description("Organization management");
986
- org.command("get").description("Get organization details").action(async () => {
987
- try {
988
- const gOpts = program.opts();
989
- const { client } = await createClient(gOpts.profile);
990
- renderOrg(await client.get("/org"), gOpts);
991
- } catch (err) {
992
- printError(formatError(err));
993
- process.exitCode = 1;
994
- }
995
- });
996
- org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(async (opts) => {
997
- try {
998
- const gOpts = program.opts();
999
- const body = {};
1000
- if (opts.name) body.name = opts.name;
1001
- if (opts.contactEmail) body.contact_email = opts.contactEmail;
1002
- if (opts.countryCode) body.country_code = opts.countryCode;
1003
- if (Object.keys(body).length === 0) {
1004
- printError("At least one of --name, --contact-email, --country-code is required");
1005
- process.exitCode = 2;
1006
- return;
1007
- }
1008
- const { client } = await createClient(gOpts.profile);
1009
- await client.patch("/org", body);
1010
- printMutationOk("Org updated", gOpts.json);
1011
- } catch (err) {
1012
- printError(formatError(err));
1013
- process.exitCode = 1;
1876
+ org.command("get").description("Get organization details").action(withClient(program, async ({ client, gOpts }) => {
1877
+ renderOrg(await client.get("/org"), gOpts);
1878
+ }));
1879
+ org.command("update").description("Update organization details").option("--name <name>", "Organization name").option("--contact-email <email>", "Contact email address").option("--country-code <cc>", "ISO country code (e.g. DE)").action(withClient(program, async ({ client, gOpts }, opts) => {
1880
+ const body = {};
1881
+ if (opts.name) body.name = opts.name;
1882
+ if (opts.contactEmail) body.contact_email = opts.contactEmail;
1883
+ if (opts.countryCode) body.country_code = opts.countryCode;
1884
+ if (Object.keys(body).length === 0) {
1885
+ printError("At least one of --name, --contact-email, --country-code is required");
1886
+ process.exitCode = 2;
1887
+ return;
1014
1888
  }
1015
- });
1889
+ await client.patch("/org", body);
1890
+ printMutationOk("Org updated", gOpts.json);
1891
+ }));
1016
1892
  }
1017
1893
  //#endregion
1018
1894
  //#region src/timelock.ts
@@ -1032,129 +1908,122 @@ async function solveTimeLockWithProgress(baseHex, modulusHex, T, onProgress) {
1032
1908
  //#endregion
1033
1909
  //#region src/commands/register.ts
1034
1910
  function registerRegister(program) {
1035
- program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(async (opts) => {
1036
- try {
1037
- const gOpts = program.opts();
1038
- const profile = gOpts.profile ?? "default";
1039
- const apiUrl = getApiUrl();
1040
- const challengeRes = await fetch(`${apiUrl}/auth/register`, {
1041
- method: "POST",
1042
- headers: { "Content-Type": "application/json" },
1043
- body: JSON.stringify({
1044
- email: opts.email,
1045
- org_slug: opts.slug
1046
- })
1047
- });
1048
- if (challengeRes.status === 409) {
1049
- printError("Email or slug already registered");
1050
- process.exitCode = 1;
1051
- return;
1052
- }
1053
- if (challengeRes.status === 429) {
1054
- printError("Rate limited — please wait before retrying");
1055
- process.exitCode = 1;
1056
- return;
1057
- }
1058
- if (!challengeRes.ok) {
1059
- printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
1060
- process.exitCode = 1;
1061
- return;
1062
- }
1063
- const challenge = await challengeRes.json();
1064
- if (challenge.algorithm !== "rsa-timelock-v1") {
1065
- printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
1066
- process.exitCode = 1;
1067
- return;
1068
- }
1069
- process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
1070
- const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
1071
- process.stdout.write(`\rSolving PoW challenge: ${pct}%`);
1072
- });
1073
- process.stdout.write("\rSolving PoW challenge: 100%\n");
1074
- const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
1075
- method: "POST",
1076
- headers: { "Content-Type": "application/json" },
1077
- body: JSON.stringify({
1078
- challenge_id: challenge.challenge_id,
1079
- nonce,
1080
- org_name: opts.name,
1081
- org_slug: opts.slug,
1082
- consent: true
1083
- })
1084
- });
1085
- if (solveRes.status === 409) {
1086
- printError("Conflict: email or slug already registered");
1087
- process.exitCode = 1;
1088
- return;
1089
- }
1090
- if (solveRes.status === 410) {
1091
- printError("Challenge expired — please register again");
1092
- process.exitCode = 1;
1093
- return;
1094
- }
1095
- if (!solveRes.ok) {
1096
- printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
1097
- process.exitCode = 1;
1098
- return;
1099
- }
1100
- const data = await solveRes.json();
1101
- const creds = loadCredentials();
1102
- creds[profile] = {
1103
- client_id: data.client_id,
1104
- client_secret: data.client_secret
1105
- };
1106
- saveCredentials(creds);
1107
- try {
1108
- const tokenRes = await fetch(`${apiUrl}/oauth/token`, {
1109
- method: "POST",
1110
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1111
- body: new URLSearchParams({
1112
- grant_type: "client_credentials",
1113
- client_id: data.client_id,
1114
- client_secret: data.client_secret
1115
- }).toString()
1116
- });
1117
- if (tokenRes.ok) {
1118
- const tokenData = await tokenRes.json();
1119
- const cache = loadTokenCache();
1120
- cache[profile] = {
1121
- access_token: tokenData.access_token,
1122
- expires_at: Date.now() / 1e3 + tokenData.expires_in
1123
- };
1124
- saveTokenCache(cache);
1125
- }
1126
- } catch {
1127
- process.stderr.write("Warning: initial token cache failed\n");
1128
- }
1129
- if (gOpts.json) printJson(data);
1130
- else {
1131
- printSuccess(`Registered '${opts.name}' (${opts.slug})`);
1132
- console.log("Credentials saved to .rine/credentials.json");
1133
- console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
1134
- }
1135
- } catch (err) {
1136
- printError(formatError(err));
1911
+ program.command("register").description("Register a new organization (two-step PoW flow)").requiredOption("--email <email>", "Email address").requiredOption("--name <name>", "Organization name").requiredOption("--slug <slug>", "Organization slug").action(withErrorHandler(program, async (gOpts, opts) => {
1912
+ const profile = gOpts.profile ?? "default";
1913
+ const apiUrl = getApiUrl();
1914
+ const challengeRes = await fetch(`${apiUrl}/auth/register`, {
1915
+ method: "POST",
1916
+ headers: { "Content-Type": "application/json" },
1917
+ body: JSON.stringify({
1918
+ email: opts.email,
1919
+ org_slug: opts.slug
1920
+ })
1921
+ });
1922
+ if (challengeRes.status === 409) {
1923
+ printError("Email or slug already registered");
1137
1924
  process.exitCode = 1;
1925
+ return;
1138
1926
  }
1139
- });
1927
+ if (challengeRes.status === 429) {
1928
+ printError("Rate limited — please wait before retrying");
1929
+ process.exitCode = 1;
1930
+ return;
1931
+ }
1932
+ if (!challengeRes.ok) {
1933
+ printError(`Registration failed: ${(await challengeRes.json().catch(() => ({}))).detail ?? challengeRes.statusText}`);
1934
+ process.exitCode = 1;
1935
+ return;
1936
+ }
1937
+ const challenge = await challengeRes.json();
1938
+ if (challenge.algorithm !== "rsa-timelock-v1") {
1939
+ printError(`Unsupported algorithm: ${challenge.algorithm}. Please upgrade rine-cli.`);
1940
+ process.exitCode = 1;
1941
+ return;
1942
+ }
1943
+ process.stderr.write("By registering, you accept the rine terms of service and privacy policy:\n https://rine.network/terms.html\n https://rine.network/privacy.html\n");
1944
+ const progressStream = gOpts.json ? process.stderr : process.stdout;
1945
+ const nonce = await solveTimeLockWithProgress(challenge.prefix, challenge.modulus, challenge.difficulty, (pct) => {
1946
+ progressStream.write(`\rSolving PoW challenge: ${pct}%`);
1947
+ });
1948
+ progressStream.write("\rSolving PoW challenge: 100%\n");
1949
+ const solveRes = await fetch(`${apiUrl}/auth/register/solve`, {
1950
+ method: "POST",
1951
+ headers: { "Content-Type": "application/json" },
1952
+ body: JSON.stringify({
1953
+ challenge_id: challenge.challenge_id,
1954
+ nonce,
1955
+ org_name: opts.name,
1956
+ org_slug: opts.slug,
1957
+ consent: true
1958
+ })
1959
+ });
1960
+ if (solveRes.status === 409) {
1961
+ printError("Conflict: email or slug already registered");
1962
+ process.exitCode = 1;
1963
+ return;
1964
+ }
1965
+ if (solveRes.status === 410) {
1966
+ printError("Challenge expired — please register again");
1967
+ process.exitCode = 1;
1968
+ return;
1969
+ }
1970
+ if (!solveRes.ok) {
1971
+ printError(`Solve failed: ${(await solveRes.json().catch(() => ({}))).detail ?? solveRes.statusText}`);
1972
+ process.exitCode = 1;
1973
+ return;
1974
+ }
1975
+ const data = await solveRes.json();
1976
+ const creds = loadCredentials();
1977
+ creds[profile] = {
1978
+ client_id: data.client_id,
1979
+ client_secret: data.client_secret
1980
+ };
1981
+ saveCredentials(creds);
1982
+ try {
1983
+ cacheToken(profile, await fetchOAuthToken(data.client_id, data.client_secret));
1984
+ } catch {
1985
+ process.stderr.write("Warning: initial token cache failed\n");
1986
+ }
1987
+ if (gOpts.json) printJson(data);
1988
+ else {
1989
+ printSuccess(`Registered '${opts.name}' (${opts.slug})`);
1990
+ console.log("Credentials saved to .rine/credentials.json");
1991
+ console.log("Next: create an agent with 'rine agent create --name <agent-name>'");
1992
+ }
1993
+ }));
1140
1994
  }
1141
1995
  //#endregion
1142
1996
  //#region src/commands/stream.ts
1143
- function formatMessageLine(dataStr) {
1997
+ async function formatMessageLine(dataStr, agentId, client) {
1144
1998
  try {
1145
1999
  const data = JSON.parse(dataStr);
1146
- return `[${(data.created_at ?? "").slice(0, 19) || "?"}] ${data.sender_handle ?? String(data.from_agent_id ?? "?")} \u2192 ${data.type ?? "?"}: ${data.payload ? JSON.stringify(data.payload).slice(0, 60) : ""}`;
2000
+ const ts = (data.created_at ?? "").slice(0, 19) || "?";
2001
+ const sender = data.sender_handle ?? String(data.from_agent_id ?? "?");
2002
+ const msgType = data.type ?? "?";
2003
+ const target = data.group_handle ? `${data.group_handle} (${msgType})` : msgType;
2004
+ let preview = "[encrypted]";
2005
+ if (data.encrypted_payload && agentKeysExist(agentId)) try {
2006
+ let result;
2007
+ if (data.encryption_version === "sender-key-v1" && data.group_id) result = await decryptGroupMessage(agentId, data.group_id, data.encrypted_payload, client);
2008
+ else if (data.encryption_version === "hpke-v1") {
2009
+ result = await decryptMessage(agentId, data.encrypted_payload, client);
2010
+ ingestSenderKeyDistribution(agentId, data.type ?? "", result);
2011
+ } else return `[${ts}] ${sender} \u2192 ${target}: [encrypted]`;
2012
+ preview = `[${result.verified ? "✓" : "?"}] ${result.plaintext.slice(0, 60)}`;
2013
+ } catch {
2014
+ preview = "[decrypt failed]";
2015
+ }
2016
+ return `[${ts}] ${sender} \u2192 ${target}: ${preview}`;
1147
2017
  } catch {
1148
2018
  return `[message] ${dataStr.slice(0, 80)}`;
1149
2019
  }
1150
2020
  }
1151
2021
  function registerStream(program) {
1152
- program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID (auto-resolved if omitted)").option("--verbose", "Show heartbeats and reconnect details").action(async (opts) => {
2022
+ program.command("stream").description("Stream incoming messages via SSE").option("--agent <id>", "Agent ID to stream (defaults to your agent)").option("--verbose", "Show heartbeats and reconnect details").action(async (opts) => {
1153
2023
  try {
1154
2024
  const gOpts = program.opts();
1155
2025
  const { client, profileName, entry } = await createClient(gOpts.profile);
1156
- let agentId = opts.agent;
1157
- if (!agentId) agentId = await resolveFirstAgent(client);
2026
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
1158
2027
  const url = `${getApiUrl()}/agents/${agentId}/stream`;
1159
2028
  let lastEventId;
1160
2029
  let backoff = 1;
@@ -1169,7 +2038,7 @@ function registerStream(program) {
1169
2038
  Accept: "text/event-stream"
1170
2039
  };
1171
2040
  if (lastEventId) headers["Last-Event-ID"] = lastEventId;
1172
- await new Promise((resolve, reject) => {
2041
+ await new Promise((resolve) => {
1173
2042
  if (stopped) {
1174
2043
  resolve();
1175
2044
  return;
@@ -1184,7 +2053,7 @@ function registerStream(program) {
1184
2053
  id,
1185
2054
  data
1186
2055
  }));
1187
- else if (event === "message") console.log(formatMessageLine(data));
2056
+ else if (event === "message") formatMessageLine(data, agentId, client).then((line) => console.log(line), () => console.log(`[message] ${data.slice(0, 80)}`));
1188
2057
  else if (event === "heartbeat" && opts.verbose) process.stderr.write(`[heartbeat] ${data}\n`);
1189
2058
  },
1190
2059
  onDisconnect: () => resolve(),
@@ -1204,7 +2073,7 @@ function registerStream(program) {
1204
2073
  process.once("SIGINT", onStop);
1205
2074
  });
1206
2075
  backoff = 1;
1207
- } catch (err) {
2076
+ } catch {
1208
2077
  if (stopped) break;
1209
2078
  if (opts.verbose) process.stderr.write(`Reconnecting in ${backoff}s...\n`);
1210
2079
  else process.stderr.write("Reconnecting...\n");
@@ -1221,125 +2090,86 @@ function registerStream(program) {
1221
2090
  //#region src/commands/webhook.ts
1222
2091
  function registerWebhook(program) {
1223
2092
  const webhook = program.command("webhook").description("Manage webhooks");
1224
- webhook.command("create").description("Create a new webhook").requiredOption("--agent <id>", "Agent ID to attach webhook to").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(async (opts) => {
1225
- const gOpts = program.opts();
2093
+ webhook.command("create").description("Create a new webhook").option("--agent <id>", "Agent ID (defaults to your agent)").requiredOption("--url <url>", "Webhook target URL (must be https://)").action(withClient(program, async ({ client, gOpts }, opts) => {
1226
2094
  if (!opts.url.startsWith("https://")) {
1227
2095
  printError("Webhook URL must start with https://");
1228
2096
  process.exitCode = 2;
1229
2097
  return;
1230
2098
  }
1231
- try {
1232
- const { client } = await createClient(gOpts.profile);
1233
- const data = await client.post("/webhooks", {
1234
- agent_id: opts.agent,
1235
- url: opts.url
1236
- });
1237
- if (gOpts.json) printJson(data);
1238
- else printTable([{
1239
- ID: data.id,
1240
- "Agent ID": data.agent_id,
1241
- URL: data.url,
1242
- Secret: data.secret,
1243
- Active: data.active,
1244
- Created: data.created_at
1245
- }]);
1246
- } catch (err) {
1247
- printError(formatError(err));
1248
- process.exitCode = 1;
1249
- }
1250
- });
1251
- webhook.command("list").description("List webhooks").option("--agent <id>", "Filter by agent ID").option("--include-inactive", "Include inactive webhooks").action(async (opts) => {
1252
- const gOpts = program.opts();
1253
- try {
1254
- const { client } = await createClient(gOpts.profile);
1255
- const params = {};
1256
- if (opts.agent) params.agent_id = opts.agent;
1257
- if (opts.includeInactive) params.include_inactive = true;
1258
- const data = await client.get("/webhooks", params);
1259
- if (gOpts.json) printJson(data);
1260
- else printTable((data.items ?? []).map((w) => ({
1261
- ID: w.id,
1262
- "Agent ID": w.agent_id,
1263
- URL: w.url,
1264
- Active: w.active,
1265
- Created: w.created_at
1266
- })));
1267
- } catch (err) {
1268
- printError(formatError(err));
1269
- process.exitCode = 1;
1270
- }
1271
- });
1272
- webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(async (webhookId, opts) => {
1273
- const gOpts = program.opts();
1274
- try {
1275
- if (!opts.yes && !gOpts.json) {
1276
- if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
1277
- }
1278
- const { client } = await createClient(gOpts.profile);
1279
- await client.delete(`/webhooks/${webhookId}`);
1280
- printMutationOk("Webhook deleted", gOpts.json);
1281
- } catch (err) {
1282
- printError(formatError(err));
1283
- process.exitCode = 1;
1284
- }
1285
- });
1286
- webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
1287
- const gOpts = program.opts();
1288
- try {
1289
- const { client } = await createClient(gOpts.profile);
1290
- await client.patch(`/webhooks/${webhookId}`, { active: false });
1291
- printMutationOk("Webhook deactivated", gOpts.json);
1292
- } catch (err) {
1293
- printError(formatError(err));
1294
- process.exitCode = 1;
1295
- }
1296
- });
1297
- webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(async (webhookId) => {
1298
- const gOpts = program.opts();
1299
- try {
1300
- const { client } = await createClient(gOpts.profile);
1301
- await client.patch(`/webhooks/${webhookId}`, { active: true });
1302
- printMutationOk("Webhook activated", gOpts.json);
1303
- } catch (err) {
1304
- printError(formatError(err));
1305
- process.exitCode = 1;
1306
- }
1307
- });
1308
- webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(async (webhookId, opts) => {
1309
- const gOpts = program.opts();
1310
- try {
1311
- const { client } = await createClient(gOpts.profile);
1312
- const params = {};
1313
- if (opts.status) params.status = opts.status;
1314
- if (opts.limit) params.limit = opts.limit;
1315
- const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
1316
- if (gOpts.json) printJson(data);
1317
- else printTable((data.items ?? []).map((d) => ({
1318
- ID: d.id,
1319
- "Message ID": d.message_id,
1320
- Status: d.status,
1321
- Attempts: d.attempts,
1322
- "Last Error": d.last_error ?? "—",
1323
- Created: d.created_at
1324
- })));
1325
- } catch (err) {
1326
- printError(formatError(err));
1327
- process.exitCode = 1;
2099
+ const agentId = await resolveAgent(client, opts.agent, gOpts.as);
2100
+ const data = await client.post("/webhooks", {
2101
+ agent_id: agentId,
2102
+ url: opts.url
2103
+ });
2104
+ if (gOpts.json) printJson(data);
2105
+ else printTable([{
2106
+ ID: data.id,
2107
+ "Agent ID": data.agent_id,
2108
+ URL: data.url,
2109
+ Secret: data.secret,
2110
+ Active: data.active,
2111
+ Created: data.created_at
2112
+ }]);
2113
+ }));
2114
+ webhook.command("list").description("List webhooks").option("--agent <id>", "Filter by agent ID").option("--include-inactive", "Include inactive webhooks").action(withClient(program, async ({ client, gOpts }, opts) => {
2115
+ const params = {};
2116
+ if (opts.agent) params.agent_id = opts.agent;
2117
+ if (opts.includeInactive) params.include_inactive = true;
2118
+ const data = await client.get("/webhooks", params);
2119
+ if (gOpts.json) printJson(data);
2120
+ else printTable(data.items.map((w) => ({
2121
+ ID: w.id,
2122
+ "Agent ID": w.agent_id,
2123
+ URL: w.url,
2124
+ Active: w.active,
2125
+ Created: w.created_at
2126
+ })));
2127
+ }));
2128
+ webhook.command("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("--yes", "Skip confirmation prompt").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
2129
+ if (!opts.yes && !gOpts.json) {
2130
+ if (!await promptConfirm(`Delete webhook ${webhookId}?`)) return;
1328
2131
  }
1329
- });
2132
+ await client.delete(`/webhooks/${webhookId}`);
2133
+ printMutationOk("Webhook deleted", gOpts.json);
2134
+ }));
2135
+ webhook.command("deactivate").description("Deactivate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
2136
+ await client.patch(`/webhooks/${webhookId}`, { active: false });
2137
+ printMutationOk("Webhook deactivated", gOpts.json);
2138
+ }));
2139
+ webhook.command("activate").description("Activate a webhook").argument("<webhook-id>", "Webhook ID").action(withClient(program, async ({ client, gOpts }, webhookId) => {
2140
+ await client.patch(`/webhooks/${webhookId}`, { active: true });
2141
+ printMutationOk("Webhook activated", gOpts.json);
2142
+ }));
2143
+ webhook.command("deliveries").description("List deliveries for a webhook").argument("<webhook-id>", "Webhook ID").option("--status <status>", "Filter by delivery status").option("--limit <n>", "Maximum number of deliveries").action(withClient(program, async ({ client, gOpts }, webhookId, opts) => {
2144
+ const params = {};
2145
+ if (opts.status) params.status = opts.status;
2146
+ if (opts.limit) params.limit = opts.limit;
2147
+ const data = await client.get(`/webhooks/${webhookId}/deliveries`, params);
2148
+ if (gOpts.json) printJson(data);
2149
+ else printTable(data.items.map((d) => ({
2150
+ ID: d.id,
2151
+ "Message ID": d.message_id,
2152
+ Status: d.status,
2153
+ Attempts: d.attempts,
2154
+ "Last Error": d.last_error ?? "—",
2155
+ Created: d.created_at
2156
+ })));
2157
+ }));
1330
2158
  }
1331
2159
  //#endregion
1332
2160
  //#region src/main.ts
1333
2161
  const { version } = createRequire(import.meta.url)("../package.json");
1334
- const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table");
2162
+ const program = new Command("rine").version(version).description("rine.network CLI — messaging infrastructure for AI agents").option("--profile <name>", "credential profile to use", "default").option("--json", "output as JSON").option("--table", "output as table").option("--as <agent-uuid>", "act as a specific agent (sets X-Rine-Agent header)");
1335
2163
  registerAuth(program);
1336
2164
  registerRegister(program);
1337
2165
  registerOrg(program);
1338
2166
  registerAgent(program);
1339
2167
  registerAgentProfile(program);
1340
2168
  registerMessages(program);
2169
+ registerGroup(program);
1341
2170
  registerDiscover(program);
1342
2171
  registerWebhook(program);
2172
+ registerKeys(program);
1343
2173
  registerStream(program);
1344
2174
  program.parse();
1345
2175
  //#endregion