From 7893c706e593cb8ee607b2616d19ed866fcab8e5 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sun, 6 Sep 2026 20:54:01 +0200 Subject: [PATCH] fix(fleet): resolve registered worker node from SDK metadata Session-Id: 01a0780a-52c0-7140-8c30-1a9b57898d53 --- .../relay-fleet-client.registration.test.ts | 56 +++++++++++++++++++ src/fleet/relay-fleet-client.ts | 14 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 src/fleet/relay-fleet-client.registration.test.ts diff --git a/src/fleet/relay-fleet-client.registration.test.ts b/src/fleet/relay-fleet-client.registration.test.ts new file mode 100644 index 00000000..bdec1ce9 --- /dev/null +++ b/src/fleet/relay-fleet-client.registration.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { RelaycastMessagingClient, type RelaycastMessagingOptions } from '@agent-relay/sdk/messaging' + +import { RelayFleetClient } from './relay-fleet-client' + +// Exercise the published SDK normalizer: RelayAgent has metadata.fleet, not a +// top-level node field. A fake RelayMessaging returning { node: 'sandbox' } +// bypasses this boundary and cannot reproduce the registration failure. +describe('remote registration through the Relay SDK', () => { + it.each([ + ['node_id', 'id'], + ['nodeId', 'nodeId'], + ])('resolves fleet metadata %s against roster %s without weakening admission', async (agentIdKey, nodeIdKey) => { + const agent = { + id: 'worker-1', name: 'worker', status: 'active', + metadata: { fleet: { [agentIdKey]: 'node_sandbox' } }, + } + const presence = { agent_id: 'worker-1', agent_name: 'worker', status: 'online' } + const node = { + [nodeIdKey]: 'node_sandbox', name: 'sandbox', status: 'online', live: true, + capabilities: [{ name: 'spawn:codex' }], + } + const messaging = new RelaycastMessagingClient({ + relaycast: { + agents: { list: async () => [agent], presence: async () => [presence] }, + nodes: { list: async () => [node] }, + } as unknown as RelaycastMessagingOptions['relaycast'], + }) + const fleet = new RelayFleetClient({ messaging, registerLifecycleAction: false }) + const input = { name: 'worker', node: 'sandbox', capability: 'spawn:codex' as const } + + expect((await messaging.agents.list())[0]).not.toHaveProperty('node') + await expect(fleet.isAgentRegistered(input)).resolves.toBe(true) + expect((await fleet.roster()).agents).toEqual([{ name: 'worker', node: 'sandbox' }]) + + // The same online name on another node must not satisfy this placement. + await expect(fleet.isAgentRegistered({ ...input, node: 'other-sandbox' })).resolves.toBe(false) + agent.metadata.fleet[agentIdKey] = 'node_missing' + await expect(fleet.isAgentRegistered(input)).resolves.toBe(false) + // A node ID must resolve through the node roster, never by treating it as a name. + agent.metadata.fleet[agentIdKey] = 'sandbox' + await expect(fleet.isAgentRegistered(input)).resolves.toBe(false) + agent.metadata.fleet[agentIdKey] = 'node_sandbox' + + presence.status = 'offline' + await expect(fleet.isAgentRegistered(input)).resolves.toBe(false) + presence.status = 'online' + node.live = false + await expect(fleet.isAgentRegistered(input)).resolves.toBe(false) + node.live = true + node.capabilities = [{ name: 'spawn:claude' }] + await expect(fleet.isAgentRegistered(input)).resolves.toBe(false) + node.capabilities = [{ name: 'spawn:codex' }] + await expect(fleet.isAgentRegistered(input)).resolves.toBe(true) + }) +}) diff --git a/src/fleet/relay-fleet-client.ts b/src/fleet/relay-fleet-client.ts index d3ed324c..966a8482 100644 --- a/src/fleet/relay-fleet-client.ts +++ b/src/fleet/relay-fleet-client.ts @@ -688,12 +688,24 @@ export class RelayFleetClient implements FleetClient { messaging.nodes.list(), ]) const agentsByName = new Map(agents.map((agent) => [agent.name, agent])) + const nodeNamesById = new Map() + for (const node of nodes) { + if (node.id) nodeNamesById.set(node.id, node.name) + if (node.nodeId) nodeNamesById.set(node.nodeId, node.name) + } return { agents: presence .filter((agent) => agent.status === 'online') .map((agent) => { const record = asRecord(agentsByName.get(agent.agentName)) - const node = readString(record, 'node', 'node_id', 'nodeId') + // The SDK preserves placement in metadata.fleet, whose node ID is + // distinct from the name returned by placement.spawn. Resolve it + // against this roster before comparing registration to placement. + const fleet = asRecord(asRecord(record?.metadata)?.fleet) + const nodeId = readString(fleet, 'node_id', 'nodeId') + const node = nodeId + ? nodeNamesById.get(nodeId) + : readString(record, 'node', 'node_id', 'nodeId') return { name: agent.agentName, ...(node ? { node } : {}) } }), nodes: nodes.map((node) => ({