@agentvault/claude-bridge 0.5.2 → 0.5.4

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/index.js +108 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,10 +10,19 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // src/config.ts
13
- import { existsSync } from "node:fs";
13
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
14
14
  import { join as join5 } from "node:path";
15
+ function hasRecoverableBackup(dataDir) {
16
+ try {
17
+ const parsed = JSON.parse(readFileSync2(join5(dataDir, BACKUP_FILE), "utf-8"));
18
+ return !!(parsed && parsed.deviceId && parsed.deviceJwt && parsed.sessions && Object.keys(parsed.sessions).length > 0);
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
15
23
  function hasPersistedCreds(dataDir) {
16
- return CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)));
24
+ if (CRED_FILES.some((f7) => existsSync(join5(dataDir, f7)))) return true;
25
+ return hasRecoverableBackup(dataDir);
17
26
  }
18
27
  function slugify2(name) {
19
28
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
@@ -78,11 +87,12 @@ function loadConfig(env, argv = []) {
78
87
  armRoom
79
88
  };
80
89
  }
81
- var CRED_FILES;
90
+ var CRED_FILES, BACKUP_FILE;
82
91
  var init_config = __esm({
83
92
  "src/config.ts"() {
84
93
  "use strict";
85
94
  CRED_FILES = ["agentvault.json", "secure-channel.json"];
95
+ BACKUP_FILE = "agentvault.json.bak";
86
96
  }
87
97
  });
88
98
 
@@ -332,7 +342,7 @@ var init_systemd = __esm({
332
342
  });
333
343
 
334
344
  // src/service/backend.ts
335
- import { execFileSync } from "node:child_process";
345
+ import { spawnSync } from "node:child_process";
336
346
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, rmSync as rmSync3, existsSync as existsSync4, chmodSync } from "node:fs";
337
347
  import { userInfo } from "node:os";
338
348
  function defaultDeps() {
@@ -341,13 +351,8 @@ function defaultDeps() {
341
351
  uid: userInfo().uid,
342
352
  user: userInfo().username,
343
353
  exec: (cmd, args) => {
344
- try {
345
- const stdout = execFileSync(cmd, args, { encoding: "utf8" });
346
- return { code: 0, stdout };
347
- } catch (e7) {
348
- const err = e7;
349
- return { code: err.status ?? 1, stdout: String(err.stdout ?? "") };
350
- }
354
+ const r7 = spawnSync(cmd, args, { encoding: "utf8" });
355
+ return { code: r7.status ?? 1, stdout: r7.stdout ?? "" };
351
356
  },
352
357
  writeFile: (p2, d10) => writeFileSync2(p2, d10),
353
358
  mkdir: (p2) => mkdirSync3(p2, { recursive: true }),
@@ -64555,6 +64560,11 @@ function migratePersistedState(raw) {
64555
64560
  messageHistory: []
64556
64561
  };
64557
64562
  }
64563
+ function _isSelfHubAddress(ownHubAddress, target) {
64564
+ const own = (ownHubAddress ?? "").trim().toLowerCase();
64565
+ const tgt = (target ?? "").trim().toLowerCase();
64566
+ return own.length > 0 && own === tgt;
64567
+ }
64558
64568
  var ROOM_AGENT_TYPES;
64559
64569
  var CREDENTIAL_MESSAGE_TYPES;
64560
64570
  var CATCHUP_MESSAGE_TYPES;
@@ -64626,6 +64636,15 @@ var init_channel = __esm2({
64626
64636
  _ackTimer = null;
64627
64637
  _stopped = false;
64628
64638
  _persisted = null;
64639
+ // AgentVault display_name, resolved from GET /hub/identities (#499, A1).
64640
+ // hub_address alone isn't enough — its random suffix (wren-8789ee6d…) differs
64641
+ // from what the owner types (Wren). Empty until resolved; the DM preamble is
64642
+ // then omitted rather than asserting a wrong name. Re-resolved on every
64643
+ // connect (picks up renames; retries after a failed fetch).
64644
+ _ownDisplayName = "";
64645
+ // Dedupes concurrent resolves (connect-trigger + lazy DM-trigger) so a burst
64646
+ // of DMs can't stampede /hub/identities.
64647
+ _resolvingOwnDisplayName = null;
64629
64648
  _httpServer = null;
64630
64649
  _mcpServer = null;
64631
64650
  _pollFallbackTimer = null;
@@ -64657,7 +64676,7 @@ var init_channel = __esm2({
64657
64676
  _pendingMlsKpBundle;
64658
64677
  /**
64659
64678
  * Pool of pending KeyPackage bundles this device has published but not yet
64660
- * consumed by a Welcome. The backend now consumes published KeyPackages FIFO,
64679
+ * consumed by a Welcome. The backend consumes published KeyPackages newest-first (#508),
64661
64680
  * so a given room's creator may have been served ANY of these — `_handleMlsWelcome`
64662
64681
  * trial-decrypts the incoming Welcome against every bundle in the pool (plus the
64663
64682
  * connect KP and the persisted per-group bundle). Capped at `_KP_POOL_TARGET`;
@@ -64895,6 +64914,50 @@ var init_channel = __esm2({
64895
64914
  get deviceId() {
64896
64915
  return this._deviceId;
64897
64916
  }
64917
+ /** This agent's AgentVault display_name (#499, A1). "" until resolved. */
64918
+ get ownDisplayName() {
64919
+ return this._ownDisplayName;
64920
+ }
64921
+ /** This agent's AgentVault hub_address (#499, A1). "" until known. */
64922
+ get ownHubAddress() {
64923
+ return this._persisted?.hubAddress ?? "";
64924
+ }
64925
+ /** Public best-effort trigger to (re)resolve this agent's display_name.
64926
+ * Called lazily from the 1:1 DM path when the name is still unknown, so a
64927
+ * resolve that failed at connect (or a fresh agent that hadn't registered
64928
+ * yet) is retried without waiting for the next WS reconnect (#499, A1). */
64929
+ ensureOwnIdentityResolved() {
64930
+ return this._resolveOwnDisplayName();
64931
+ }
64932
+ /** Resolve this agent's own display_name from GET /api/v1/hub/identities
64933
+ * (matches its own device_id). Deduped — concurrent callers share one
64934
+ * in-flight fetch. Refreshes the cached value on success (picks up a rename);
64935
+ * on failure leaves the prior value intact. Best-effort — never throws.
64936
+ * Mirrors Hermes RestClient.get_own_identity (#499, Component 0). */
64937
+ _resolveOwnDisplayName() {
64938
+ if (this._resolvingOwnDisplayName) return this._resolvingOwnDisplayName;
64939
+ const p2 = (async () => {
64940
+ const jwt22 = this._persisted?.deviceJwt ?? this._deviceJwt;
64941
+ const deviceId = this._persisted?.deviceId ?? this._deviceId;
64942
+ if (!jwt22 || !deviceId) return;
64943
+ try {
64944
+ const res = await fetch(`${this.config.apiUrl}/api/v1/hub/identities`, {
64945
+ headers: { Authorization: `Bearer ${jwt22}` }
64946
+ });
64947
+ if (!res.ok) return;
64948
+ const list = await res.json();
64949
+ if (!Array.isArray(list)) return;
64950
+ const mine = list.find((x22) => x22?.device_id === deviceId);
64951
+ if (mine?.display_name) this._ownDisplayName = String(mine.display_name);
64952
+ } catch {
64953
+ }
64954
+ })();
64955
+ this._resolvingOwnDisplayName = p2;
64956
+ void p2.finally(() => {
64957
+ if (this._resolvingOwnDisplayName === p2) this._resolvingOwnDisplayName = null;
64958
+ });
64959
+ return p2;
64960
+ }
64898
64961
  get fingerprint() {
64899
64962
  return this._fingerprint;
64900
64963
  }
@@ -66418,6 +66481,11 @@ var init_channel = __esm2({
66418
66481
  if (!this._persisted?.hubAddress) {
66419
66482
  throw new Error("This agent does not have a hub address assigned");
66420
66483
  }
66484
+ if (_isSelfHubAddress(this._persisted.hubAddress, responderHubAddress)) {
66485
+ throw new Error(
66486
+ "Refusing to open an A2A channel to this agent's own hub address (self-reference). The owner is addressing you by your own name \u2014 reply to them directly instead of opening a channel to yourself."
66487
+ );
66488
+ }
66421
66489
  if (!this._deviceJwt) {
66422
66490
  throw new Error("Channel not authenticated");
66423
66491
  }
@@ -66498,6 +66566,11 @@ var init_channel = __esm2({
66498
66566
  * Falls back to plaintext for channels without a session (legacy/pre-encryption).
66499
66567
  */
66500
66568
  async sendToAgent(hubAddress, text, opts) {
66569
+ if (_isSelfHubAddress(this._persisted?.hubAddress ?? "", hubAddress)) {
66570
+ throw new Error(
66571
+ "Refusing to send an A2A message to this agent's own hub address (self-reference) \u2014 reply to the owner directly."
66572
+ );
66573
+ }
66501
66574
  if (!this._persisted?.a2aChannels) {
66502
66575
  try {
66503
66576
  await this.listA2AChannels();
@@ -67122,8 +67195,7 @@ var init_channel = __esm2({
67122
67195
  return;
67123
67196
  }
67124
67197
  if (data.event === "device_revoked") {
67125
- await clearState(this.config.dataDir);
67126
- this._handleError(new Error("Device was revoked"));
67198
+ this._handleDeviceRevoked();
67127
67199
  return;
67128
67200
  }
67129
67201
  if (data.event === "device_linked") {
@@ -67347,6 +67419,7 @@ var init_channel = __esm2({
67347
67419
  this._persisted.agentHubId = data.data.hub_id;
67348
67420
  this._persisted.agentRole = data.data.agent_role ?? "peer";
67349
67421
  if (changed) this._persistState();
67422
+ void this._resolveOwnDisplayName();
67350
67423
  if (!this._telemetryReporter && this._persisted.deviceJwt && this._persisted.hubId) {
67351
67424
  this._telemetryReporter = new TelemetryReporter({
67352
67425
  apiBase: this.config.apiUrl,
@@ -70521,6 +70594,24 @@ ${messageText}`;
70521
70594
  this._setState("error");
70522
70595
  this.emit("error", err);
70523
70596
  }
70597
+ /**
70598
+ * Handle a WS `device_revoked` event. Surfaces a terminal error so
70599
+ * attachLifecycle stops the channel — but does NOT delete credentials.
70600
+ *
70601
+ * This signal fires both for a genuine owner revoke AND for a transient
70602
+ * session-takeover of a device that is still ACTIVE on the server (the error
70603
+ * text even reads "or another session has taken over"). Deleting
70604
+ * agentvault.json here — as this path used to via clearState() — strands a
70605
+ * still-valid identity: the primary creds vanish, only the .bak survives, and
70606
+ * the bridge's token-gate then demands a fresh invite token to restart.
70607
+ * Keeping the creds means a restart reconnects via restoreState() when the
70608
+ * device is still valid, and fails cleanly (device_revoked again, no data
70609
+ * lost) when it isn't. Re-enrollment, which SHOULD wipe creds, goes through
70610
+ * the explicit setup --force / _forceReEnroll path in start().
70611
+ */
70612
+ _handleDeviceRevoked() {
70613
+ this._handleError(new Error("Device was revoked"));
70614
+ }
70524
70615
  /**
70525
70616
  * Persist all ratchet session states to disk.
70526
70617
  * Syncs live ratchet states back into the persisted sessions map.
@@ -132493,7 +132584,7 @@ var ArmingState = class {
132493
132584
  };
132494
132585
 
132495
132586
  // src/approve-cli.ts
132496
- import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132587
+ import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync as rmSync2, existsSync as existsSync3 } from "node:fs";
132497
132588
  import { join as join6 } from "node:path";
132498
132589
  var APPROVALS_SUBDIR = "arm-approvals";
132499
132590
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
@@ -132526,7 +132617,7 @@ function drainApprovals(dataDir) {
132526
132617
  if (!ID_RE.test(name)) continue;
132527
132618
  let roomId = "";
132528
132619
  try {
132529
- roomId = readFileSync3(join6(dir, name), "utf8").trim();
132620
+ roomId = readFileSync4(join6(dir, name), "utf8").trim();
132530
132621
  } catch {
132531
132622
  }
132532
132623
  out.push({ requestId: name, roomId: ID_RE.test(roomId) ? roomId : "" });
@@ -132775,7 +132866,7 @@ async function main() {
132775
132866
  "[bridge] warning: passing the invite token on the command line is visible to other local users via 'ps'. Prefer: AV_INVITE_TOKEN=\u2026 npx @agentvault/claude-bridge"
132776
132867
  );
132777
132868
  }
132778
- console.error(`[bridge] version: ${true ? "0.5.2" : "dev"}`);
132869
+ console.error(`[bridge] version: ${true ? "0.5.4" : "dev"}`);
132779
132870
  console.error(`[bridge] data dir: ${cfg.dataDir} (${cfg.dataDirSource})`);
132780
132871
  if (cfg.worker) {
132781
132872
  console.error(`[bridge] WORKER MODE \u2014 workspace: ${cfg.workspaceDir} \xB7 permissions: ${cfg.permissionMode} \xB7 enclave dir fenced`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentvault/claude-bridge",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "type": "module",
5
5
  "description": "AgentVault Claude Bridge — daemon for bridging a Claude agent into secure E2E-encrypted AgentVault 1:1 direct messages and rooms.",
6
6
  "main": "dist/index.js",