@snowyroad/arp 0.6.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.
File without changes
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) {
@@ -784,20 +901,17 @@ function extractJsonObject(raw) {
784
901
  }
785
902
  function buildPartialCard(agentName, self) {
786
903
  return {
787
- protocolVersion: "0.3.0",
788
904
  name: agentName,
789
905
  description: self.description,
790
- capabilities: { streaming: false, pushNotifications: false, stateTransitionHistory: false },
906
+ capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
791
907
  defaultInputModes: ["text/plain"],
792
908
  defaultOutputModes: ["text/plain"],
793
909
  skills: self.skills,
794
- preferredTransport: "JSONRPC",
795
- additionalInterfaces: [],
910
+ supportedInterfaces: [{ protocolBinding: "JSONRPC", protocolVersion: "1.0" }],
796
911
  iconUrl: "",
797
912
  documentationUrl: "",
798
913
  securitySchemes: {},
799
914
  security: [],
800
- supportsAuthenticatedExtendedCard: false,
801
915
  signatures: []
802
916
  };
803
917
  }
@@ -1293,6 +1407,14 @@ var RelayClient = class {
1293
1407
  case "roster_update": {
1294
1408
  const m = msg.member ?? {};
1295
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
+ }
1296
1418
  const entry = normalizeRosterEntry(m.name, m.description, m.card);
1297
1419
  this.rosterCbs.forEach((cb) => cb(entry));
1298
1420
  }
@@ -1466,7 +1588,17 @@ var RelayClient = class {
1466
1588
  }
1467
1589
  const body = await res.json();
1468
1590
  const members = body?.channel?.members ?? [];
1469
- 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
+ });
1470
1602
  } catch (err) {
1471
1603
  console.warn("[arp-bridge] roster fetch failed:", sanitizeForTty(String(err)));
1472
1604
  return [];
@@ -3168,6 +3300,7 @@ async function publishSelfCard(cfg, relay, session) {
3168
3300
  } else {
3169
3301
  card = buildPartialCard(cfg.agentName, { description: "", skills: [] });
3170
3302
  }
3303
+ if (cfg.signCard) card = await cfg.signCard(card);
3171
3304
  await relay.putAgentCard(card);
3172
3305
  }
3173
3306
  function learnRoster(relay, channelId, session) {
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/arp",
3
- "version": "0.6.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",
@@ -20,7 +20,6 @@
20
20
  "engines": {
21
21
  "node": ">=20"
22
22
  },
23
- "packageManager": "pnpm@9.15.4",
24
23
  "bin": {
25
24
  "arp": "dist/cli.js"
26
25
  },
@@ -29,21 +28,13 @@
29
28
  "README.md",
30
29
  "LICENSE.md"
31
30
  ],
32
- "scripts": {
33
- "build": "tsup",
34
- "prepublishOnly": "pnpm build",
35
- "dev": "tsx src/index.ts",
36
- "join": "tsx src/index.ts",
37
- "test": "vitest run",
38
- "test:watch": "vitest",
39
- "test:integration": "vitest run --config vitest.integration.config.ts",
40
- "typecheck": "tsc --noEmit"
41
- },
42
31
  "dependencies": {
43
32
  "@agentclientprotocol/sdk": "0.25.1",
44
33
  "@anthropic-ai/claude-agent-sdk": "0.3.177",
45
34
  "@anthropic-ai/sdk": "0.104.1",
46
35
  "@modelcontextprotocol/sdk": "1.29.0",
36
+ "canonicalize": "^3.0.0",
37
+ "jose": "^6.2.3",
47
38
  "ws": "8.21.0",
48
39
  "zod": "4.4.3"
49
40
  },
@@ -54,5 +45,14 @@
54
45
  "tsx": "^4.19.0",
55
46
  "typescript": "^6.0.3",
56
47
  "vitest": "^2.1.0"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsx src/index.ts",
52
+ "join": "tsx src/index.ts",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:integration": "vitest run --config vitest.integration.config.ts",
56
+ "typecheck": "tsc --noEmit"
57
57
  }
58
- }
58
+ }