Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions apps/local-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) ===
Expand Down
162 changes: 159 additions & 3 deletions apps/local-server/test/blank-world-action-production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<Record<string, unknown>> = [];

const hermes = createHttpServer((request, response) => {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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");
Expand Down
50 changes: 34 additions & 16 deletions apps/web/src/world-entry/AgentSetupMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -86,6 +92,11 @@ function InstallationForm({
identityId,
displayName: displayName.trim(),
...(conversationRef ? { conversationRef } : {}),
...(registration &&
identityId === registration.identity.id &&
conversationRef === (registration.conversationRef ?? "")
? { replaceConnectionId: registration.id }
: {}),
});
}}
>
Expand Down Expand Up @@ -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 (
<section className="agent-setup-environment" data-setup-environment={kind}>
{kind === "wsl" && distributions.length > 1 ? (
Expand Down Expand Up @@ -279,15 +290,20 @@ function EnvironmentInstallations({
</details>
) : null}
{installation ? (
<InstallationForm
key={installation.id}
installation={installation}
registration={registration}
check={checks[registration?.id ?? ""]}
busy={busy}
onAttach={onAttach}
onAddAgent={onAddAgent}
/>
(matchingRegistrations.length
? matchingRegistrations
: [undefined]
).map((registration) => (
<InstallationForm
key={`${installation.id}:${registration?.id ?? "new"}`}
installation={installation}
registration={registration}
check={checks[registration?.id ?? ""]}
busy={busy}
onAttach={onAttach}
onAddAgent={onAddAgent}
/>
))
) : (
<p>
{environment?.message ??
Expand Down Expand Up @@ -560,7 +576,9 @@ export function AgentSetupMenu({
})
}
>
Use v{newer.version}
{newer.version
? `Use v${newer.version}`
: "Use discovered installation"}
</button>
) : null;
})()}
Expand Down
16 changes: 7 additions & 9 deletions apps/web/src/world-entry/agent-setup-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
);
}
Loading
Loading