From 056b2a47d98835e17452f418c01cc0f3c1a82d91 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 13 Jul 2026 11:23:19 +0200 Subject: [PATCH] fix(hub): harden relationship boundaries --- .../server/src/server/agent/agent-storage.ts | 19 ++++++++++++++----- .../hub/test-utils/relationship-harness.ts | 5 +++-- .../websocket-server.notifications.test.ts | 1 + ...cket-server.terminal-notifications.test.ts | 1 + .../server/src/server/websocket-server.ts | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index f78c0a4d66..8d48454235 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -91,6 +91,7 @@ export class AgentStorage { private pendingWrites: Map> = new Map(); private deleting: Set = new Set(); private hubAgentIdsByExecution: Map = new Map(); + private hubExecutionKeysByAgentId: Map = new Map(); private loaded = false; private baseDir: string; private loadPromise: Promise | null = null; @@ -260,6 +261,7 @@ export class AgentStorage { this.pathById.clear(); this.pathsById.clear(); this.hubAgentIdsByExecution.clear(); + this.hubExecutionKeysByAgentId.clear(); try { const records = await this.scanDisk(); @@ -366,16 +368,23 @@ export class AgentStorage { private indexOwner(record: StoredAgentRecord): void { this.removeOwnerIndex(record.id); if (record.owner?.kind === "hub") { - this.hubAgentIdsByExecution.set(hubExecutionKey(record.owner), record.id); + const key = hubExecutionKey(record.owner); + const previousAgentId = this.hubAgentIdsByExecution.get(key); + if (previousAgentId && previousAgentId !== record.id) { + this.hubExecutionKeysByAgentId.delete(previousAgentId); + } + this.hubAgentIdsByExecution.set(key, record.id); + this.hubExecutionKeysByAgentId.set(record.id, key); } } private removeOwnerIndex(agentId: string): void { - for (const [key, indexedAgentId] of this.hubAgentIdsByExecution) { - if (indexedAgentId === agentId) { - this.hubAgentIdsByExecution.delete(key); - } + const key = this.hubExecutionKeysByAgentId.get(agentId); + if (!key) return; + if (this.hubAgentIdsByExecution.get(key) === agentId) { + this.hubAgentIdsByExecution.delete(key); } + this.hubExecutionKeysByAgentId.delete(agentId); } private async waitForPendingWrite(agentId: string): Promise { diff --git a/packages/server/src/server/hub/test-utils/relationship-harness.ts b/packages/server/src/server/hub/test-utils/relationship-harness.ts index 482f7d039c..05666d67a1 100644 --- a/packages/server/src/server/hub/test-utils/relationship-harness.ts +++ b/packages/server/src/server/hub/test-utils/relationship-harness.ts @@ -58,6 +58,7 @@ import type { const execFileAsync = promisify(execFile); const HUB_ORIGIN = "https://hub.test"; const SOCKET_URL = "wss://hub.test/daemon"; +const REPOSITORY_ROOT = path.resolve(import.meta.dirname, "../../../../../.."); export interface ArchiveWatcher { close(): void; @@ -1013,11 +1014,11 @@ export class HubRelationshipHarness { } private async runCli(args: string[]): Promise> { - const entrypoint = path.join(process.cwd(), "packages/cli/src/index.ts"); + const entrypoint = path.join(REPOSITORY_ROOT, "packages/cli/src/index.ts"); const { stdout } = await execFileAsync( process.execPath, ["--import", "tsx", entrypoint, ...args, "--host", this.host, "--json"], - { cwd: process.cwd(), env: { ...process.env, NO_COLOR: "1" } }, + { cwd: REPOSITORY_ROOT, env: { ...process.env, NO_COLOR: "1" } }, ); const parsed = JSON.parse(stdout) as unknown; if (Array.isArray(parsed)) return parsed[0] as Record; diff --git a/packages/server/src/server/websocket-server.notifications.test.ts b/packages/server/src/server/websocket-server.notifications.test.ts index e0eba93629..ebf300a3ae 100644 --- a/packages/server/src/server/websocket-server.notifications.test.ts +++ b/packages/server/src/server/websocket-server.notifications.test.ts @@ -188,6 +188,7 @@ function connectClient( ) { const ws = createOpenSocket(); asInternals(server).sessions.set(ws, { + kind: "trusted", session: createSessionWithActivity(activity), clientId: "client-test", appVersion: null, diff --git a/packages/server/src/server/websocket-server.terminal-notifications.test.ts b/packages/server/src/server/websocket-server.terminal-notifications.test.ts index 3862f5b4ff..7d3cba294f 100644 --- a/packages/server/src/server/websocket-server.terminal-notifications.test.ts +++ b/packages/server/src/server/websocket-server.terminal-notifications.test.ts @@ -198,6 +198,7 @@ function createOpenSocket() { function connectClient(server: VoiceAssistantWebSocketServer) { const ws = createOpenSocket(); asInternals<{ sessions: Map }>(server).sessions.set(ws, { + kind: "trusted", session: { getClientActivity: vi.fn(() => null), }, diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 0cc52331d0..d220a08de0 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -2262,7 +2262,7 @@ function toSessionPeer(identity: WebSocketConnectionIdentity): SessionPeer { function isLoopbackAddress(address: string): boolean { const normalized = address.toLowerCase(); - if (normalized === "::1") return true; + if (normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true; const ipv4 = normalized.startsWith("::ffff:") ? normalized.slice("::ffff:".length) : normalized; return ipv4.startsWith("127."); }