@snowyroad/arp 0.7.0 → 0.8.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 (2) hide show
  1. package/dist/cli.js +139 -3
  2. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -125,13 +125,17 @@ function parseStoredAgent(file) {
125
125
  }
126
126
  const tm = a.toolMode;
127
127
  const toolMode = tm === "readonly" || tm === "full" ? tm : void 0;
128
+ const signingPrivateJwk = typeof a.signingPrivateJwk === "string" && a.signingPrivateJwk.trim() !== "" ? a.signingPrivateJwk : void 0;
129
+ const signingKid = typeof a.signingKid === "string" && a.signingKid.trim() !== "" ? a.signingKid : void 0;
128
130
  return {
129
131
  relayUrl: a.relayUrl.trim(),
130
132
  agentId: a.agentId.trim(),
131
133
  agentName: a.agentName.trim(),
132
134
  agentUuid: a.agentUuid.trim(),
133
135
  agentKey: a.agentKey.trim(),
134
- ...toolMode ? { toolMode } : {}
136
+ ...toolMode ? { toolMode } : {},
137
+ ...signingPrivateJwk ? { signingPrivateJwk } : {},
138
+ ...signingKid ? { signingKid } : {}
135
139
  };
136
140
  }
137
141
  function listAgents(dir) {
@@ -505,6 +509,98 @@ async function chooseFirstRunToolMode(agentName, io = { input: process.stdin, ou
505
509
  return { mode: "readonly", persist: false };
506
510
  }
507
511
 
512
+ // src/cardSigning.ts
513
+ import canonicalize from "canonicalize";
514
+ import {
515
+ FlattenedSign,
516
+ flattenedVerify,
517
+ importJWK,
518
+ exportJWK,
519
+ calculateJwkThumbprint,
520
+ generateKeyPair
521
+ } from "jose";
522
+ var SIGNING_ALG = "EdDSA";
523
+ var REQUIRED_TOP = /* @__PURE__ */ new Set(["name", "description", "skills"]);
524
+ var RELAY_OWNED_TOP = ["version", "provider"];
525
+ function isEmptyValue(v) {
526
+ if (v === "" || v === null || v === void 0) return true;
527
+ if (Array.isArray(v)) return v.length === 0;
528
+ if (typeof v === "object") return Object.keys(v).length === 0;
529
+ return false;
530
+ }
531
+ function pruneNode(node, requiredKeys) {
532
+ if (Array.isArray(node)) return node.map((e) => e && typeof e === "object" ? pruneNode(e, /* @__PURE__ */ new Set()) : e);
533
+ if (!node || typeof node !== "object") return node;
534
+ const out = {};
535
+ for (const [k, v] of Object.entries(node)) {
536
+ const pv = v && typeof v === "object" ? pruneNode(v, /* @__PURE__ */ new Set()) : v;
537
+ if (requiredKeys.has(k)) {
538
+ out[k] = pv;
539
+ continue;
540
+ }
541
+ if (isEmptyValue(pv)) continue;
542
+ out[k] = pv;
543
+ }
544
+ return out;
545
+ }
546
+ function pruneForSigning(card) {
547
+ const clone = JSON.parse(JSON.stringify(card ?? {}));
548
+ delete clone.signatures;
549
+ for (const f of RELAY_OWNED_TOP) delete clone[f];
550
+ if (Array.isArray(clone.supportedInterfaces)) {
551
+ clone.supportedInterfaces = clone.supportedInterfaces.map((i) => {
552
+ if (i && typeof i === "object") {
553
+ const { url, ...rest } = i;
554
+ return rest;
555
+ }
556
+ return i;
557
+ });
558
+ }
559
+ return pruneNode(clone, REQUIRED_TOP);
560
+ }
561
+ function canonicalCardString(card) {
562
+ return canonicalize(pruneForSigning(card));
563
+ }
564
+ function publicOkpJwk(jwk) {
565
+ return { kty: jwk.kty, crv: jwk.crv, x: jwk.x };
566
+ }
567
+ async function generateSigningKey() {
568
+ const { privateKey } = await generateKeyPair(SIGNING_ALG, { crv: "Ed25519", extractable: true });
569
+ const privateJwk = await exportJWK(privateKey);
570
+ const kid = await calculateJwkThumbprint(publicOkpJwk(privateJwk));
571
+ return { privateJwk, kid };
572
+ }
573
+ async function signCard(card, privateJwk) {
574
+ const key = await importJWK(privateJwk, SIGNING_ALG);
575
+ const pub = publicOkpJwk(privateJwk);
576
+ const kid = await calculateJwkThumbprint(pub);
577
+ const payload = new TextEncoder().encode(canonicalCardString(card));
578
+ const jws = await new FlattenedSign(payload).setProtectedHeader({ alg: SIGNING_ALG, typ: "JOSE", kid, jwk: pub }).sign(key);
579
+ return [{ protected: jws.protected, signature: jws.signature }];
580
+ }
581
+ async function verifyCardSignature(card) {
582
+ const sigs = card?.signatures;
583
+ if (!Array.isArray(sigs) || sigs.length === 0) return { status: "unsigned" };
584
+ const payload = Buffer.from(canonicalCardString(card), "utf8").toString("base64url");
585
+ for (const sig of sigs) {
586
+ try {
587
+ if (!sig || typeof sig.protected !== "string" || typeof sig.signature !== "string") continue;
588
+ const header = JSON.parse(Buffer.from(sig.protected, "base64url").toString("utf8"));
589
+ if (header.alg !== SIGNING_ALG || !header.jwk) continue;
590
+ const pub = publicOkpJwk(header.jwk);
591
+ const kid = await calculateJwkThumbprint(pub);
592
+ if (header.kid && header.kid !== kid) continue;
593
+ const key = await importJWK(pub, SIGNING_ALG);
594
+ await flattenedVerify({ protected: sig.protected, payload, signature: sig.signature }, key, {
595
+ algorithms: [SIGNING_ALG]
596
+ });
597
+ return { status: "valid", kid };
598
+ } catch {
599
+ }
600
+ }
601
+ return { status: "invalid" };
602
+ }
603
+
508
604
  // src/config.ts
509
605
  var DEFAULT_MODEL = "claude-opus-4-8";
510
606
  var DEFAULT_AGENT_MODE = "acp";
@@ -654,6 +750,26 @@ async function buildFromStoredAgent(dir, stored, env) {
654
750
  return inflight;
655
751
  };
656
752
  const token = await mintToken();
753
+ let signingPrivateJwk = current.signingPrivateJwk;
754
+ if (!signingPrivateJwk) {
755
+ try {
756
+ const gen = await generateSigningKey();
757
+ signingPrivateJwk = JSON.stringify(gen.privateJwk);
758
+ current = { ...current, signingPrivateJwk, signingKid: gen.kid };
759
+ saveAgent(dir, current);
760
+ } catch (e) {
761
+ console.warn("[arp-bridge] signing key generation failed; cards will be unsigned:", String(e));
762
+ }
763
+ }
764
+ const signCardFn = signingPrivateJwk ? async (card) => {
765
+ try {
766
+ const sigs = await signCard(card, JSON.parse(signingPrivateJwk));
767
+ return { ...card, signatures: sigs };
768
+ } catch (e) {
769
+ console.warn("[arp-bridge] card signing failed; publishing unsigned:", String(e));
770
+ return card;
771
+ }
772
+ } : void 0;
657
773
  return {
658
774
  relayWsUrl,
659
775
  relayHttpUrl,
@@ -668,7 +784,8 @@ async function buildFromStoredAgent(dir, stored, env) {
668
784
  catchUpTtlMs: positiveIntEnv(env.ARP_CATCHUP_TTL_MS, 72e5),
669
785
  catchUpMaxMentions: positiveIntEnv(env.ARP_CATCHUP_MAX_MENTIONS, 3),
670
786
  mintToken,
671
- agentFile: file
787
+ agentFile: file,
788
+ ...signCardFn ? { signCard: signCardFn } : {}
672
789
  };
673
790
  }
674
791
  async function loadConfigFromInvite(code, env) {
@@ -1290,6 +1407,14 @@ var RelayClient = class {
1290
1407
  case "roster_update": {
1291
1408
  const m = msg.member ?? {};
1292
1409
  if (typeof m.name === "string" && m.name.length > 0) {
1410
+ if (m.card && typeof m.card === "object") {
1411
+ void verifyCardSignature(m.card).then((r) => {
1412
+ if (r.status === "invalid") {
1413
+ console.warn(`[arp-bridge] peer card signature invalid for "${m.name}" (card not trusted for integrity)`);
1414
+ }
1415
+ }).catch(() => {
1416
+ });
1417
+ }
1293
1418
  const entry = normalizeRosterEntry(m.name, m.description, m.card);
1294
1419
  this.rosterCbs.forEach((cb) => cb(entry));
1295
1420
  }
@@ -1463,7 +1588,17 @@ var RelayClient = class {
1463
1588
  }
1464
1589
  const body = await res.json();
1465
1590
  const members = body?.channel?.members ?? [];
1466
- return members.filter((m) => m?.type === "bot" && typeof m.id === "string").map((m) => normalizeRosterEntry(m.id, m.description, m.card));
1591
+ return members.filter((m) => m?.type === "bot" && typeof m.id === "string").map((m) => {
1592
+ if (m.card && typeof m.card === "object") {
1593
+ void verifyCardSignature(m.card).then((r) => {
1594
+ if (r.status === "invalid") {
1595
+ console.warn(`[arp-bridge] peer card signature invalid for "${m.id}" (card not trusted for integrity)`);
1596
+ }
1597
+ }).catch(() => {
1598
+ });
1599
+ }
1600
+ return normalizeRosterEntry(m.id, m.description, m.card);
1601
+ });
1467
1602
  } catch (err) {
1468
1603
  console.warn("[arp-bridge] roster fetch failed:", sanitizeForTty(String(err)));
1469
1604
  return [];
@@ -3165,6 +3300,7 @@ async function publishSelfCard(cfg, relay, session) {
3165
3300
  } else {
3166
3301
  card = buildPartialCard(cfg.agentName, { description: "", skills: [] });
3167
3302
  }
3303
+ if (cfg.signCard) card = await cfg.signCard(card);
3168
3304
  await relay.putAgentCard(card);
3169
3305
  }
3170
3306
  function learnRoster(relay, channelId, session) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Connect your own coding agent (Claude Code, Codex, Gemini, Grok) to an Agent Relay Protocol channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",
@@ -33,6 +33,8 @@
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.177",
34
34
  "@anthropic-ai/sdk": "0.104.1",
35
35
  "@modelcontextprotocol/sdk": "1.29.0",
36
+ "canonicalize": "^3.0.0",
37
+ "jose": "^6.2.3",
36
38
  "ws": "8.21.0",
37
39
  "zod": "4.4.3"
38
40
  },