From 4431fd1d7c4a9130bb61a5309fed7389836bc30e Mon Sep 17 00:00:00 2001 From: MelaBuilt AI <282087817+MelaBuilt-AI@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:00:42 -0400 Subject: [PATCH] fix: bind movement and setup to exact saved agent connections --- apps/local-server/src/index.ts | 24 ++- .../blank-world-action-production.test.ts | 162 +++++++++++++++++- apps/web/src/world-entry/AgentSetupMenu.tsx | 50 ++++-- .../world-entry/agent-setup-presentation.ts | 16 +- .../fixtures/saved-connections-component.tsx | 108 ++++++++++++ .../test/saved-connections-component.test.tsx | 125 ++++++++++++++ 6 files changed, 453 insertions(+), 32 deletions(-) create mode 100644 apps/web/test/fixtures/saved-connections-component.tsx create mode 100644 apps/web/test/saved-connections-component.test.tsx diff --git a/apps/local-server/src/index.ts b/apps/local-server/src/index.ts index 7ec3f17..51a23c5 100644 --- a/apps/local-server/src/index.ts +++ b/apps/local-server/src/index.ts @@ -5,6 +5,7 @@ import { import { AgentIntersectReadClient } from "@agentintersect-world/agentintersect-client/read"; import { WorldEventStore } from "@agentintersect-world/persistence"; import { + AgentCapabilityManifestSchema, PHASE19_ADAPTER_IDS, capabilitySnapshotHash, } from "@agentintersect-world/agent-session-protocol"; @@ -644,10 +645,25 @@ if (config !== undefined && coordinationGitConfig !== undefined) { ) ) return null; - const manifest = ( - await agentAdapterRegistry.capabilities() - ).find(({ adapterId }) => adapterId === session.adapterId); - if (!manifest) return null; + let manifest; + try { + // Match chat's exact saved connection, not the harness default. + const adapter = agentAdapterRegistry.require( + session.adapterId, + session.connectionId, + ); + manifest = AgentCapabilityManifestSchema.parse( + await adapter.attest(), + ); + if ( + manifest.adapterId !== session.adapterId || + !manifest.capabilities.attach || + !manifest.capabilities.sendText + ) + return null; + } catch { + return null; + } const worldActionsEnabled = manifest.capabilities.worldActions && capabilitySnapshotHash(manifest) === diff --git a/apps/local-server/test/blank-world-action-production.test.ts b/apps/local-server/test/blank-world-action-production.test.ts index fff63f5..2c0700d 100644 --- a/apps/local-server/test/blank-world-action-production.test.ts +++ b/apps/local-server/test/blank-world-action-production.test.ts @@ -64,7 +64,7 @@ async function waitFor( throw new Error(`timed out waiting for ${description}`); } -async function fixture() { +async function fixture(savedCodex = false) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "aiw-blank-actions-")); roots.push(root); const pluginRoot = path.join(root, "plugin", "agentintersect-world"); @@ -85,6 +85,74 @@ async function fixture() { mode: 0o600, }); + const connectionId = randomUUID(); + if (savedCodex) { + const executablePath = path.join(root, "codex-fixture.mjs"); + fs.writeFileSync( + executablePath, + `#!${process.execPath} +import { randomUUID } from "node:crypto"; +const args = process.argv.slice(2); +for await (const chunk of process.stdin) void chunk; +if (args.includes("--version")) console.log("codex-cli 0.149.1"); +else if (args.includes("--help")) console.log("Usage: codex exec resume [SESSION_ID] --model --sandbox --json"); +else { + const resume = args.indexOf("resume"); + const thread = resume < 0 ? randomUUID() : args[resume + 1]; + for (const event of [ + { type: "thread.started", thread_id: thread }, + { type: "turn.started" }, + { type: "item.completed", item: { id: "reply", type: "agent_message", text: "Fixture reply" } }, + { type: "turn.completed", usage: { input_tokens: 1, cached_input_tokens: 0, output_tokens: 1 } }, + ]) console.log(JSON.stringify(event)); +} +`, + { mode: 0o700 }, + ); + const profilePath = path.join(root, "codex-profile"); + fs.mkdirSync(profilePath); + const registration = { + installationId: "fixture", + adapterId: "codex", + environment: { + id: process.platform === "win32" ? "windows" : "linux", + kind: process.platform === "win32" ? "windows" : "linux", + label: "Native fixture", + }, + homePath: root, + identity: { + id: "default", + label: "Default", + kind: "profile", + profilePath, + }, + connectedAt: new Date().toISOString(), + }; + const setupDirectory = path.join(root, "agent-setup"); + fs.mkdirSync(setupDirectory); + fs.writeFileSync( + path.join(setupDirectory, "agent-setup.json"), + JSON.stringify({ + schema: "aiw.agent-setup/1", + completed: true, + registrations: [ + { + ...registration, + id: randomUUID(), + displayName: "Codex", + executablePath: path.join(root, "missing-codex.mjs"), + }, + { + ...registration, + id: connectionId, + displayName: "Codex2", + executablePath, + }, + ], + }), + ); + } + const turnBodies: Array> = []; const hermes = createHttpServer((request, response) => { @@ -174,7 +242,7 @@ async function fixture() { AIW_PORT: String(serverPort), AIW_AGENTINTERSECT_ENABLED: "false", AIW_AGENTINTERSECT_COMMANDS_ENABLED: "false", - AIW_AGENT_SESSIONS_ENABLED: "true", + AIW_AGENT_SESSIONS_ENABLED: savedCodex ? "false" : "true", AIW_AGENT_SESSION_DATA_DIR: path.join(root, "agent-sessions"), AIW_HERMES_API_URL: `http://127.0.0.1:${hermesPort}`, AIW_HERMES_API_KEY: "fixture-key", @@ -222,7 +290,15 @@ async function fixture() { [key: string]: unknown; }; }; - return { attach, baseUrl, proposalRoot, repositoryRoot, turnBodies }; + return { + attach, + baseUrl, + proposalRoot, + repositoryRoot, + turnBodies, + connectionId, + root, + }; } async function indexRepository(baseUrl: string, rootPath: string) { @@ -258,6 +334,86 @@ afterEach(async () => { }); describe("production blank-World movement authority", () => { + it("uses the exact saved Codex connection when the default adapter is unavailable", async () => { + const state = await fixture(true); + const capabilities = await fetch( + `${state.baseUrl}/agent-sessions/capabilities`, + ); + expect((await capabilities.json()).data).toEqual([]); + const created = await fetch(`${state.baseUrl}/agent-sessions/world`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + connectionId: state.connectionId, + adapterId: "codex", + worldInstanceId: "saved-connection-movement", + displayName: "Codex2", + profile: "default", + workspaceId: "world-entry", + repositoryRef: "current", + mode: "explore", + }), + }); + const createdBody = await created.json(); + expect(created.status, JSON.stringify(createdBody)).toBe(201); + const session = createdBody.data; + expect(session).toMatchObject({ + status: "ready", + connectionId: state.connectionId, + }); + const movementUrl = `${state.baseUrl}/world-actions/${session.sessionId}`; + const initial = await fetch(movementUrl); + expect(initial.status, await initial.clone().text()).toBe(200); + // Native autonomous tools remain disabled; local human direction is distinct. + expect(await initial.json()).toMatchObject({ + capability: { enabled: false }, + }); + const action = { + kind: "move-agent", + schema: "aiw.agent-movement/1", + actorId: session.sessionId, + source: "user-directed", + speed: 4, + target: { kind: "follow-user", stoppingRadius: 2 }, + }; + const propose = (candidate: typeof action) => + fetch(`${movementUrl}/proposals`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ actions: [candidate] }), + }); + const follow = await propose(action); + expect(follow.status, await follow.clone().text()).toBe(202); + const snapshot = await fetch(movementUrl); + expect(snapshot.status).toBe(200); + expect((await snapshot.json()).executions).toEqual( + expect.arrayContaining([expect.objectContaining({ accepted: true })]), + ); + expect( + (await propose({ ...action, source: "agent-autonomous" })).status, + ).toBe(409); + expect( + (await propose({ ...action, actorId: randomUUID() })).status, + ).not.toBe(202); + + // A healthy default must never substitute for an unavailable saved connection. + fs.renameSync( + path.join(state.root, "codex-fixture.mjs"), + path.join(state.root, "missing-codex.mjs"), + ); + const recoveredDefault = await fetch( + `${state.baseUrl}/agent-sessions/capabilities`, + ); + expect((await recoveredDefault.json()).data).toEqual([ + expect.objectContaining({ adapterId: "codex" }), + ]); + const unavailable = await fetch(movementUrl); + expect(unavailable.status).toBe(404); + expect(await unavailable.json()).toEqual({ + error: "World session is unavailable", + }); + }); + it("constructs an exact-session Hermes turn with bounded move and follow authority", async () => { const state = await fixture(); const session = await state.attach("native-chat-movement", "current"); diff --git a/apps/web/src/world-entry/AgentSetupMenu.tsx b/apps/web/src/world-entry/AgentSetupMenu.tsx index c9ad403..d70f751 100644 --- a/apps/web/src/world-entry/AgentSetupMenu.tsx +++ b/apps/web/src/world-entry/AgentSetupMenu.tsx @@ -10,7 +10,7 @@ import type { import "./agent-setup.css"; import { LocalVoiceSetup } from "./LocalVoiceSetup.js"; import { - installationRegistration, + installationRegistrations, setupEnvironmentLabel, } from "./agent-setup-presentation.js"; import { InstallationSetupActions } from "./InstallationSetupActions.js"; @@ -23,7 +23,7 @@ type AttachInput = { replaceConnectionId?: string | undefined; }; -/** A newer discovered install of the same harness and environment, if the saved one is older. */ +/** A newer install, or the sole matching replacement for a vanished saved install. */ function newerInstallation( registration: AgentRegistration, discovery: DiscoveryResult | null, @@ -32,10 +32,16 @@ function newerInstallation( (i) => i.adapterId === registration.adapterId && i.environment.id === registration.environment.id && - i.identities.some((identity) => identity.id === registration.identity.id), + i.homePath === registration.homePath && + i.identities.some( + (identity) => + identity.id === registration.identity.id && + identity.profilePath === registration.identity.profilePath, + ), ); const newest = same[0]; const saved = same.find((i) => i.id === registration.installationId); + if (!saved && same.length === 1) return newest; return newest?.version && newest.id !== registration.installationId && newest.version !== saved?.version @@ -86,6 +92,11 @@ function InstallationForm({ identityId, displayName: displayName.trim(), ...(conversationRef ? { conversationRef } : {}), + ...(registration && + identityId === registration.identity.id && + conversationRef === (registration.conversationRef ?? "") + ? { replaceConnectionId: registration.id } + : {}), }); }} > @@ -227,13 +238,13 @@ function EnvironmentInstallations({ const environment = discovery.environments.find( (e) => e.id === `wsl:${distro}`, ); - const registration = installation - ? installationRegistration( + const matchingRegistrations = installation + ? installationRegistrations( installation, registrations, discovery.currentWslDistro, ) - : undefined; + : []; return (
{kind === "wsl" && distributions.length > 1 ? ( @@ -279,15 +290,20 @@ function EnvironmentInstallations({ ) : null} {installation ? ( - + (matchingRegistrations.length + ? matchingRegistrations + : [undefined] + ).map((registration) => ( + + )) ) : (

{environment?.message ?? @@ -560,7 +576,9 @@ export function AgentSetupMenu({ }) } > - Use v{newer.version} + {newer.version + ? `Use v${newer.version}` + : "Use discovered installation"} ) : null; })()} diff --git a/apps/web/src/world-entry/agent-setup-presentation.ts b/apps/web/src/world-entry/agent-setup-presentation.ts index 3b1fd07..513bb7d 100644 --- a/apps/web/src/world-entry/agent-setup-presentation.ts +++ b/apps/web/src/world-entry/agent-setup-presentation.ts @@ -21,23 +21,21 @@ export function setupEnvironmentLabel( : "Linux (Native)"; } -export function installationRegistration( +export function installationRegistrations( installation: AgentInstallation, registrations: readonly AgentRegistration[], currentDistro?: string, -): AgentRegistration | undefined { - return ( - registrations.find((r) => r.installationId === installation.id) ?? - registrations.find( - (r) => - r.adapterId === installation.adapterId && +): readonly AgentRegistration[] { + return registrations.filter( + (r) => + r.installationId === installation.id || + (r.adapterId === installation.adapterId && r.executablePath === installation.executablePath && r.homePath === installation.homePath && (r.environment.id === installation.environment.id || (r.environment.kind === "wsl" && installation.environment.kind === "wsl" && r.environment.id === "local" && - installation.environment.distro === currentDistro)), - ) + installation.environment.distro === currentDistro))), ); } diff --git a/apps/web/test/fixtures/saved-connections-component.tsx b/apps/web/test/fixtures/saved-connections-component.tsx new file mode 100644 index 0000000..1ee904d --- /dev/null +++ b/apps/web/test/fixtures/saved-connections-component.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; +import { createRoot } from "react-dom/client"; +import type { + AgentRegistration, + AgentSetupState, +} from "@agentintersect-world/world-schema/agent-setup"; +import { AgentSetupMenu } from "../../src/world-entry/AgentSetupMenu.js"; +import "../../src/styles.css"; + +const installation = { + id: "current-install", + adapterId: "codex" as const, + environment: { id: "windows", kind: "windows" as const, label: "Windows" }, + executablePath: + "C:\\Program Files\\WindowsApps\\OpenAI.Codex_current\\app\\resources\\codex.exe", + homePath: "C:\\Users\\Fixture", + identities: [ + { + id: "default", + label: "Default", + kind: "profile" as const, + profilePath: "C:\\Users\\Fixture\\.codex", + }, + ], + status: "found" as const, +}; +const registration: AgentRegistration = { + id: "11111111-1111-4111-8111-111111111111", + adapterId: "codex", + installationId: installation.id, + environment: installation.environment, + executablePath: installation.executablePath, + homePath: installation.homePath, + identity: installation.identities[0]!, + displayName: "Codex", + connectedAt: "2026-10-12T00:00:00Z", +}; +export function Fixture() { + const repaired = new URLSearchParams(location.search).has("repaired"); + const [state, setState] = useState({ + schema: "aiw.agent-setup/1", + completed: true, + registrations: [ + { + ...registration, + ...(repaired + ? {} + : { + installationId: "old-install", + executablePath: "C:\\Old\\codex.exe", + }), + }, + { + ...registration, + id: "22222222-2222-4222-8222-222222222222", + displayName: "Codex2", + }, + ], + }); + const [result, setResult] = useState(null); + return ( + <> + r.installationId === installation.id) + .map((r) => [ + r.id, + { status: "ready" as const, message: "Fixture verified" }, + ]), + )} + onDiscover={() => {}} + onAttach={(input) => { + setResult(input); + setState((current) => ({ + ...current, + registrations: current.registrations.map((r) => + r.id === input.replaceConnectionId + ? { + ...r, + installationId: input.installationId, + executablePath: installation.executablePath, + displayName: input.displayName, + } + : r, + ), + })); + }} + onRecheck={(id) => setResult({ recheck: id })} + onAddAgent={(id) => setResult({ add: id })} + onComplete={() => {}} + /> +

+ + ); +} +createRoot(document.getElementById("root")!).render(); diff --git a/apps/web/test/saved-connections-component.test.tsx b/apps/web/test/saved-connections-component.test.tsx new file mode 100644 index 0000000..dd13a5e --- /dev/null +++ b/apps/web/test/saved-connections-component.test.tsx @@ -0,0 +1,125 @@ +import { mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { chromium, expect as browserExpect } from "@playwright/test"; +import { createServer } from "vite"; +import react from "@vitejs/plugin-react"; +import { expect, it } from "vitest"; + +it.each([false, true])( + "repairs versionless installations and selects exact saved connections (already repaired: %s; isolated component)", + async (repaired) => { + const server = await createServer({ + configFile: false, + root: fileURLToPath(new URL("..", import.meta.url)), + plugins: [ + react(), + { + name: "saved-connections-fixture", + configureServer(vite) { + vite.middlewares.use( + "/__saved_connections", + async (_request, response) => { + response.setHeader("content-type", "text/html"); + response.end( + await vite.transformIndexHtml( + "/__saved_connections", + '
', + ), + ); + }, + ); + }, + }, + ], + server: { host: "127.0.0.1", port: 0 }, + }); + await server.listen(); + const browser = await chromium.launch({ headless: true }); + try { + for (const viewport of [ + { width: 1440, height: 1000 }, + { width: 390, height: 844 }, + ]) { + const page = await browser.newPage({ viewport }); + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.route("**/api/**", (route) => + route.fulfill({ json: { ok: true, data: {} } }), + ); + await page.goto( + `${server.resolvedUrls!.local[0]}__saved_connections${repaired ? "?repaired=1" : ""}`, + ); + if (!repaired) { + const repair = page.getByRole("button", { + name: "Use discovered installation", + exact: true, + }); + await browserExpect(repair).toHaveCount(1); + await repair.click(); + expect( + JSON.parse((await page.locator("#result").textContent()) ?? "null"), + ).toMatchObject({ + installationId: "current-install", + identityId: "default", + displayName: "Codex", + replaceConnectionId: "11111111-1111-4111-8111-111111111111", + }); + await browserExpect(repair).toHaveCount(0); + } + await page.locator('[data-setup-harness="codex"] > summary').click(); + const forms = page.locator('[data-setup-harness="codex"] form'); + await browserExpect(forms).toHaveCount(2); + for (const [index, name, id] of [ + [0, "Codex", "11111111-1111-4111-8111-111111111111"], + [1, "Codex2", "22222222-2222-4222-8222-222222222222"], + ] as const) { + const form = forms.nth(index); + const connected = form.getByRole("button", { + name: new RegExp(`“${name}” — Successfully Connected`), + }); + await browserExpect(connected).toBeVisible(); + await form + .getByRole("button", { name: "Add Agent", exact: true }) + .click(); + expect( + JSON.parse((await page.locator("#result").textContent()) ?? "null"), + ).toEqual({ add: id }); + await connected.click(); + await browserExpect(form.getByLabel("Agent name")).toHaveValue(name); + await form + .getByRole("button", { + name: "Attach to Agent Intersect World", + exact: true, + }) + .click(); + expect( + JSON.parse((await page.locator("#result").textContent()) ?? "null"), + ).toMatchObject({ displayName: name, replaceConnectionId: id }); + await connected.click(); + } + await page + .locator('[data-setup-harness="codex"]') + .scrollIntoViewIfNeeded(); + for (const button of await forms + .getByRole("button", { name: "Add Agent", exact: true }) + .all()) { + const bounds = await button.boundingBox(); + expect(bounds!.x).toBeGreaterThanOrEqual(0); + expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(viewport.width); + } + if (process.env.AIW_TEST_EVIDENCE_DIR) { + await mkdir(process.env.AIW_TEST_EVIDENCE_DIR, { recursive: true }); + await page.screenshot({ + path: `${process.env.AIW_TEST_EVIDENCE_DIR}/saved-connections-${repaired}-${viewport.width}.png`, + }); + } + expect(errors).toEqual([]); + await page.close(); + } + } finally { + await browser.close(); + await server.close(); + } + }, + 60000, +);