fix(hub): harden relationship boundaries

This commit is contained in:
Mohamed Boudra
2026-07-13 11:40:20 +02:00
parent 18ba23e10a
commit 056b2a47d9
5 changed files with 20 additions and 8 deletions
@@ -91,6 +91,7 @@ export class AgentStorage {
private pendingWrites: Map<string, Promise<void>> = new Map();
private deleting: Set<string> = new Set();
private hubAgentIdsByExecution: Map<string, string> = new Map();
private hubExecutionKeysByAgentId: Map<string, string> = new Map();
private loaded = false;
private baseDir: string;
private loadPromise: Promise<StoredAgentRecord[]> | 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<void> {
@@ -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<Record<string, unknown>> {
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<string, unknown>;
@@ -188,6 +188,7 @@ function connectClient(
) {
const ws = createOpenSocket();
asInternals<WebSocketServerInternals>(server).sessions.set(ws, {
kind: "trusted",
session: createSessionWithActivity(activity),
clientId: "client-test",
appVersion: null,
@@ -198,6 +198,7 @@ function createOpenSocket() {
function connectClient(server: VoiceAssistantWebSocketServer) {
const ws = createOpenSocket();
asInternals<{ sessions: Map<unknown, unknown> }>(server).sessions.set(ws, {
kind: "trusted",
session: {
getClientActivity: vi.fn(() => null),
},
@@ -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.");
}