412 lines
16 KiB
TypeScript
412 lines
16 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { expect } from "vitest";
|
|
import { denFetch } from "@openwork/behaviors";
|
|
import type { DenSession } from "@openwork/behaviors";
|
|
import {
|
|
eventually,
|
|
needs,
|
|
queryDenDatabase,
|
|
test,
|
|
unmetNeeds,
|
|
} from "@openwork/testkit";
|
|
import type { TestNeeds } from "@openwork/testkit";
|
|
import { bootCloudModelInfra } from "../../worlds/cloud-model-infra.ts";
|
|
|
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
const requirements: TestNeeds = {
|
|
optIn: ["OPENWORK_EVAL_E2E_TESTS"],
|
|
placement: "local",
|
|
};
|
|
const missingRequirements = unmetNeeds(requirements, process.env);
|
|
const title = missingRequirements.length > 0
|
|
? `remote desktop session command verification skipped — needs: ${missingRequirements.join(", ")}`
|
|
: "a web remote-session command is owner-scoped and delivered through a capable desktop runner";
|
|
|
|
interface HttpResult {
|
|
status: number;
|
|
body: unknown;
|
|
text: string;
|
|
}
|
|
|
|
interface McpToolResult {
|
|
isError: boolean;
|
|
payload: Record<string, unknown>;
|
|
text: string;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function denId(prefix: string): string {
|
|
const alphabet = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
const bytes = randomBytes(26);
|
|
let suffix = "";
|
|
for (let index = 0; index < 26; index += 1) {
|
|
const byte = bytes[index] ?? 0;
|
|
suffix += index === 0 ? String(byte % 8) : alphabet[byte % 32];
|
|
}
|
|
return `${prefix}_${suffix}`;
|
|
}
|
|
|
|
function auth(session: DenSession): Record<string, string> {
|
|
return { authorization: `Bearer ${session.token}` };
|
|
}
|
|
|
|
function orgHeaders(session: DenSession, orgId: string): Record<string, string> {
|
|
return { ...auth(session), "x-openwork-org-id": orgId };
|
|
}
|
|
|
|
async function mintMcpToken(session: DenSession, orgId: string): Promise<string> {
|
|
const result = await denFetch(session, "/v1/mcp/token", {
|
|
method: "POST",
|
|
headers: orgHeaders(session, orgId),
|
|
body: JSON.stringify({ scopes: ["mcp:read", "mcp:write"] }),
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
});
|
|
const token = isRecord(result.body) && typeof result.body.token === "string" ? result.body.token : "";
|
|
if (!result.response.ok || !token) {
|
|
throw new Error(`Minting the MCP token failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`);
|
|
}
|
|
return token;
|
|
}
|
|
|
|
function parseMcpToolResult(result: unknown): McpToolResult {
|
|
if (!isRecord(result)) throw new Error(`MCP tools/call returned a non-object result: ${JSON.stringify(result)}`);
|
|
const content = Array.isArray(result.content) ? result.content.filter(isRecord) : [];
|
|
const text = content
|
|
.map((entry) => (typeof entry.text === "string" ? entry.text : ""))
|
|
.filter((entry) => entry.length > 0)
|
|
.join("\n");
|
|
const parsed: unknown = JSON.parse(text || "{}");
|
|
return {
|
|
isError: result.isError === true,
|
|
payload: isRecord(parsed) ? parsed : {},
|
|
text,
|
|
};
|
|
}
|
|
|
|
async function parseResponse(response: Response): Promise<HttpResult> {
|
|
const text = await response.text();
|
|
let body: unknown = null;
|
|
try {
|
|
body = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
body = text;
|
|
}
|
|
return { status: response.status, body, text };
|
|
}
|
|
|
|
test.skipIf(missingRequirements.length > 0)(title, { timeout: 15 * 60_000 }, async ({ evidence, place }) => {
|
|
needs(requirements);
|
|
await using stack = new AsyncDisposableStack();
|
|
const world = await bootCloudModelInfra(stack, place, {
|
|
daytonaApiUrl: "http://127.0.0.1:9/daytona-guard",
|
|
});
|
|
const admin = world.admin;
|
|
const orgId = world.org.id;
|
|
const mcpToken = await mintMcpToken(admin, orgId);
|
|
const databaseUrl = world.den.database?.url;
|
|
if (!databaseUrl) throw new Error("The remote-session desktop world did not expose its database.");
|
|
|
|
let requestId = 0;
|
|
const callTool = async (name: "execute_capability", args: Record<string, unknown>): Promise<McpToolResult> => {
|
|
const response = await fetch(`${world.den.ref.apiUrl}/mcp/agent`, {
|
|
method: "POST",
|
|
headers: {
|
|
authorization: `Bearer ${mcpToken}`,
|
|
"content-type": "application/json",
|
|
accept: "application/json, text/event-stream",
|
|
},
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: ++requestId,
|
|
method: "tools/call",
|
|
params: { name, arguments: args },
|
|
}),
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
});
|
|
const raw = await response.text();
|
|
if (!response.ok) throw new Error(`MCP tools/call failed: HTTP ${response.status} ${raw.slice(0, 500)}`);
|
|
const dataLine = raw.split("\n").find((line) => line.startsWith("data:"));
|
|
if (!dataLine) throw new Error(`MCP tools/call returned no SSE data frame: ${raw.slice(0, 500)}`);
|
|
const frame: unknown = JSON.parse(dataLine.slice(5));
|
|
if (!isRecord(frame)) throw new Error(`MCP tools/call returned a non-object frame: ${raw.slice(0, 500)}`);
|
|
if (frame.error) throw new Error(`MCP tools/call returned JSON-RPC error: ${JSON.stringify(frame.error)}`);
|
|
return parseMcpToolResult(frame.result);
|
|
};
|
|
|
|
const registerRunner = async (runnerId: string, capabilities: string[]): Promise<string> => {
|
|
const result = await denFetch(admin, "/v1/automation-runners/token", {
|
|
method: "POST",
|
|
headers: orgHeaders(admin, orgId),
|
|
body: JSON.stringify({
|
|
runnerId,
|
|
protocolVersion: 1,
|
|
supportedExecutionTargets: ["desktop"],
|
|
capabilities,
|
|
appVersion: "0.0.0-eval",
|
|
platform: "darwin",
|
|
concurrency: 1,
|
|
}),
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
});
|
|
const token = isRecord(result.body) && typeof result.body.token === "string" ? result.body.token : "";
|
|
if (!result.response.ok || !token) {
|
|
throw new Error(`Registering runner ${runnerId} failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`);
|
|
}
|
|
return token;
|
|
};
|
|
|
|
const runnerRequest = async (
|
|
token: string,
|
|
path: string,
|
|
init: { method?: string; body?: Record<string, unknown> } = {},
|
|
): Promise<HttpResult> => parseResponse(await fetch(`${world.den.ref.apiUrl}${path}`, {
|
|
method: init.method ?? "GET",
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
...(init.body ? { "content-type": "application/json" } : {}),
|
|
},
|
|
...(init.body ? { body: JSON.stringify(init.body) } : {}),
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
}));
|
|
|
|
const presence = async (): Promise<HttpResult> => {
|
|
const result = await denFetch(admin, "/v1/automation-runners/presence", {
|
|
headers: orgHeaders(admin, orgId),
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
});
|
|
return { status: result.response.status, body: result.body, text: result.text };
|
|
};
|
|
|
|
// Phase A: without runner presence the desktop target fails before touching Daytona.
|
|
const offline = await callTool("execute_capability", {
|
|
name: "remote-session:create",
|
|
body: { target: "desktop", title: "Desktop command", prompt: "Open the repo" },
|
|
});
|
|
expect(offline.isError).toBe(true);
|
|
expect(offline.payload.error).toBe("desktop_offline");
|
|
evidence.recordAssertionEvidence(
|
|
"Desktop remote-session creation is presence-gated",
|
|
`Before runner registration, remote-session:create returned ${offline.text}.`,
|
|
offline.isError && offline.payload.error === "desktop_offline",
|
|
);
|
|
|
|
// Phase B: ordinary Automation presence from an old runner must not open the
|
|
// remote-session capability gate.
|
|
const oldRunnerId = `eval-old-runner-${process.pid}-${Date.now().toString(36)}`;
|
|
const oldRunnerToken = await registerRunner(oldRunnerId, []);
|
|
const oldRunnerPresence = await eventually(
|
|
presence,
|
|
{
|
|
within: 30_000,
|
|
intervalMs: 500,
|
|
label: "old desktop runner presence becomes connected for Automations",
|
|
until: (result) => result.status === 200 && isRecord(result.body) && result.body.connected === true,
|
|
},
|
|
);
|
|
expect(isRecord(oldRunnerPresence.body) && oldRunnerPresence.body.connected).toBe(true);
|
|
const oldRunnerOffline = await callTool("execute_capability", {
|
|
name: "remote-session:create",
|
|
body: { target: "desktop", title: "Old runner must stay offline" },
|
|
});
|
|
expect(oldRunnerOffline.isError).toBe(true);
|
|
expect(oldRunnerOffline.payload.error).toBe("desktop_offline");
|
|
evidence.recordAssertionEvidence(
|
|
"Old runners do not open the remote-session dispatch gate",
|
|
`Runner ${oldRunnerId} registered without remote_session_v1 and counted as connected for Automations, while remote-session:create still returned ${oldRunnerOffline.text}.`,
|
|
isRecord(oldRunnerPresence.body) && oldRunnerPresence.body.connected === true
|
|
&& oldRunnerOffline.isError && oldRunnerOffline.payload.error === "desktop_offline",
|
|
);
|
|
|
|
// Phase C: capability-aware registration is the durable presence signal used by the remote-session gate.
|
|
const runnerId = `eval-runner-${process.pid}-${Date.now().toString(36)}`;
|
|
const runnerToken = await registerRunner(runnerId, ["remote_session_v1"]);
|
|
let firstPresence = await presence();
|
|
if (!isRecord(firstPresence.body) || firstPresence.body.connected !== true) {
|
|
await runnerRequest(runnerToken, "/v1/automation-runner/work");
|
|
firstPresence = await presence();
|
|
}
|
|
const connectedPresence = await eventually(
|
|
presence,
|
|
{
|
|
within: 30_000,
|
|
intervalMs: 500,
|
|
label: "desktop runner presence becomes connected",
|
|
until: (result) => result.status === 200 && isRecord(result.body) && result.body.connected === true,
|
|
},
|
|
);
|
|
expect(isRecord(connectedPresence.body) && connectedPresence.body.connected).toBe(true);
|
|
evidence.recordAssertionEvidence(
|
|
"A capable registered runner opens the desktop dispatch gate",
|
|
`Runner ${runnerId} registered and the shared presence endpoint returned ${connectedPresence.text}.`,
|
|
connectedPresence.status === 200 && isRecord(connectedPresence.body) && connectedPresence.body.connected === true,
|
|
);
|
|
|
|
// Phase D: create persists a queued command, while an old runner never sees its work item.
|
|
const queued = await callTool("execute_capability", {
|
|
name: "remote-session:create",
|
|
body: { target: "desktop", title: "Desktop command", prompt: "Open the repo" },
|
|
});
|
|
const commandId = typeof queued.payload.commandId === "string" ? queued.payload.commandId : "";
|
|
expect(queued.isError).toBe(false);
|
|
expect(queued.payload).toMatchObject({ target: "desktop", state: "queued" });
|
|
expect(commandId).toMatch(/^rsc_/);
|
|
const pendingRows = await queryDenDatabase(
|
|
databaseUrl,
|
|
"SELECT status, title, prompt FROM remote_session_command WHERE id = ?",
|
|
[commandId],
|
|
);
|
|
const pending = pendingRows.find(isRecord);
|
|
expect(pending).toMatchObject({ status: "pending", title: "Desktop command", prompt: "Open the repo" });
|
|
|
|
const oldWork = await runnerRequest(oldRunnerToken, "/v1/automation-runner/work");
|
|
const oldItems = isRecord(oldWork.body) && Array.isArray(oldWork.body.items)
|
|
? oldWork.body.items.filter(isRecord)
|
|
: [];
|
|
expect(oldWork.status).toBe(200);
|
|
expect(oldItems.some((item) => item.kind === "remote_session_create")).toBe(false);
|
|
evidence.recordAssertionEvidence(
|
|
"Queued commands are durable and hidden from old desktop runners",
|
|
`Command ${commandId} persisted pending with its title and prompt; a runner token without remote_session_v1 received ${JSON.stringify(oldItems)}.`,
|
|
pending?.status === "pending" && !oldItems.some((item) => item.kind === "remote_session_create"),
|
|
);
|
|
|
|
// Phase E: only one runner claim wins, and its assignment contains the requested input.
|
|
const capableWork = await eventually(
|
|
async () => runnerRequest(runnerToken, "/v1/automation-runner/work"),
|
|
{
|
|
within: 30_000,
|
|
intervalMs: 500,
|
|
label: "capable runner discovers remote session command",
|
|
until: (result) => isRecord(result.body)
|
|
&& Array.isArray(result.body.items)
|
|
&& result.body.items.filter(isRecord).some((item) => (
|
|
item.kind === "remote_session_create" && item.commandId === commandId
|
|
)),
|
|
},
|
|
);
|
|
const capableItems = isRecord(capableWork.body) && Array.isArray(capableWork.body.items)
|
|
? capableWork.body.items.filter(isRecord)
|
|
: [];
|
|
expect(capableItems).toContainEqual({ kind: "remote_session_create", commandId });
|
|
|
|
const claim = await runnerRequest(runnerToken, `/v1/remote-session-commands/${commandId}/claim`, { method: "POST" });
|
|
const assignment = isRecord(claim.body) && isRecord(claim.body.assignment) ? claim.body.assignment : {};
|
|
expect(claim.status).toBe(200);
|
|
expect(assignment).toMatchObject({
|
|
commandId,
|
|
kind: "remote_session_create",
|
|
title: "Desktop command",
|
|
prompt: "Open the repo",
|
|
model: null,
|
|
});
|
|
const incompleteDelivery = await runnerRequest(
|
|
runnerToken,
|
|
`/v1/remote-session-commands/${commandId}/complete`,
|
|
{ method: "POST", body: { status: "delivered" } },
|
|
);
|
|
expect(incompleteDelivery.status).toBe(400);
|
|
const contradictoryFailure = await runnerRequest(
|
|
runnerToken,
|
|
`/v1/remote-session-commands/${commandId}/complete`,
|
|
{
|
|
method: "POST",
|
|
body: {
|
|
status: "failed",
|
|
sessionId: "ses_must_not_exist",
|
|
workspaceId: "ws_must_not_exist",
|
|
error: { code: "execution_failed", message: "contradictory receipt" },
|
|
},
|
|
},
|
|
);
|
|
expect(contradictoryFailure.status).toBe(400);
|
|
const duplicateClaim = await runnerRequest(
|
|
runnerToken,
|
|
`/v1/remote-session-commands/${commandId}/claim`,
|
|
{ method: "POST" },
|
|
);
|
|
expect(duplicateClaim.status).toBe(409);
|
|
expect(duplicateClaim.body).toEqual({ error: "command_claim_conflict" });
|
|
evidence.recordAssertionEvidence(
|
|
"Desktop command claiming is compare-and-set",
|
|
`The first claim returned the requested assignment; incomplete/contradictory completions returned HTTP ${incompleteDelivery.status}/${contradictoryFailure.status}; the second claim returned HTTP ${duplicateClaim.status} ${duplicateClaim.text}.`,
|
|
claim.status === 200 && incompleteDelivery.status === 400
|
|
&& contradictoryFailure.status === 400 && duplicateClaim.status === 409,
|
|
);
|
|
|
|
// Phase F: completion becomes owner-readable through the command id.
|
|
const complete = await runnerRequest(
|
|
runnerToken,
|
|
`/v1/remote-session-commands/${commandId}/complete`,
|
|
{
|
|
method: "POST",
|
|
body: {
|
|
status: "delivered",
|
|
sessionId: "ses_eval_fake",
|
|
workspaceId: "ws_eval_fake",
|
|
resultSummary: "created",
|
|
},
|
|
},
|
|
);
|
|
expect(complete.status).toBe(200);
|
|
expect(complete.body).toEqual({
|
|
command: {
|
|
id: commandId,
|
|
status: "delivered",
|
|
sessionId: "ses_eval_fake",
|
|
workspaceId: "ws_eval_fake",
|
|
},
|
|
});
|
|
const delivered = await callTool("execute_capability", {
|
|
name: "remote-session:read",
|
|
body: { commandId },
|
|
});
|
|
expect(delivered.isError).toBe(false);
|
|
expect(delivered.payload).toMatchObject({
|
|
commandId,
|
|
target: "desktop",
|
|
state: "delivered",
|
|
sessionId: "ses_eval_fake",
|
|
workspaceId: "ws_eval_fake",
|
|
resultSummary: "created",
|
|
error: null,
|
|
});
|
|
const deliveredRows = await queryDenDatabase(
|
|
databaseUrl,
|
|
"SELECT status, session_id, workspace_id, result_summary FROM remote_session_command WHERE id = ?",
|
|
[commandId],
|
|
);
|
|
expect(deliveredRows.find(isRecord)).toMatchObject({
|
|
status: "delivered",
|
|
session_id: "ses_eval_fake",
|
|
workspace_id: "ws_eval_fake",
|
|
result_summary: "created",
|
|
});
|
|
evidence.recordAssertionEvidence(
|
|
"Runner completion is durable and readable through the gateway",
|
|
`Completion persisted delivered for ${commandId}; remote-session:read returned session ses_eval_fake and workspace ws_eval_fake.`,
|
|
delivered.payload.state === "delivered" && delivered.payload.sessionId === "ses_eval_fake",
|
|
);
|
|
|
|
const unknown = await callTool("execute_capability", {
|
|
name: "remote-session:read",
|
|
body: { commandId: denId("rsc") },
|
|
});
|
|
expect(unknown.isError).toBe(true);
|
|
expect(unknown.payload).toEqual({ error: "unknown_command" });
|
|
const malformed = await callTool("execute_capability", {
|
|
name: "remote-session:read",
|
|
body: { commandId: "not-a-command-id" },
|
|
});
|
|
expect(malformed.isError).toBe(true);
|
|
expect(malformed.payload).toEqual({ error: "unknown_command" });
|
|
evidence.recordAssertionEvidence(
|
|
"Unknown and malformed command ids do not disclose another result",
|
|
`Reading a different valid rsc id returned ${unknown.text}; a malformed id returned ${malformed.text} instead of an internal error.`,
|
|
unknown.isError && unknown.payload.error === "unknown_command"
|
|
&& malformed.isError && malformed.payload.error === "unknown_command",
|
|
);
|
|
});
|