Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
885 lines
31 KiB
JavaScript
885 lines
31 KiB
JavaScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
import { _electron as electron, chromium } from '@playwright/test';
|
|
import { execFile, spawn } from 'node:child_process';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { promisify } from 'node:util';
|
|
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { createServer } from 'node:net';
|
|
import { homedir, tmpdir } from 'node:os';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { evaluateCuE2eScenarioState, getCuE2eScenario } from './e2e-scenarios.mjs';
|
|
import { validateRealReport } from './provider-matrix.mjs';
|
|
import { sanitizeCuActionRecord, sanitizeCuReport, sanitizeCuTrace } from './report-sanitize.mjs';
|
|
import { createSqliteAgentRunStore } from '../../packages/storage/dist/agent-run-store.js';
|
|
import {
|
|
resolveStorageRoot,
|
|
tryAcquireInteractiveRootOwner,
|
|
} from '../../packages/storage/dist/root-authority.js';
|
|
import { openInteractiveRuntimePolicyStoresForWrite } from '../../packages/storage/dist/runtime-policy-stores.js';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = join(here, '../..');
|
|
const execFileAsync = promisify(execFile);
|
|
const sourceWorkspace = join(
|
|
homedir(),
|
|
'Library',
|
|
'Application Support',
|
|
'Maka',
|
|
'workspaces',
|
|
'default',
|
|
);
|
|
const scenario = getCuE2eScenario(process.env.MAKA_CU_E2E_SCENARIO ?? 'l0-observe-only');
|
|
if (!scenario.realRunEnabled) {
|
|
throw new Error(`scenario ${scenario.id} is not enabled for real-model runs`);
|
|
}
|
|
if (scenario.runner) {
|
|
throw new Error(`scenario ${scenario.id} requires dedicated runner ${scenario.runner}`);
|
|
}
|
|
const availableCapabilities = new Set(
|
|
String(process.env.MAKA_CU_EXECUTION_CAPABILITIES ?? '')
|
|
.split(',')
|
|
.map((value) => value.trim())
|
|
.filter(Boolean),
|
|
);
|
|
for (const capability of scenario.requiresExecutionCapabilities) {
|
|
if (!availableCapabilities.has(capability)) {
|
|
throw new Error(`scenario ${scenario.id} requires unavailable capability ${capability}`);
|
|
}
|
|
}
|
|
|
|
const timeoutMs = Number(process.env.MAKA_CU_REAL_MODEL_TIMEOUT_MS ?? 180_000);
|
|
const keepProfile = process.env.MAKA_CU_KEEP_PROFILE === '1';
|
|
const providerOverride = process.env.MAKA_CU_PROVIDER;
|
|
const reportPath =
|
|
process.env.MAKA_CU_REAL_MODEL_REPORT ??
|
|
join(repoRoot, '.agents-workspace-data', 'cu-real-model', `report-${Date.now()}.json`);
|
|
const runPrompt = 'Use the maka_computer tool to complete this task. ' + scenario.prompt;
|
|
|
|
async function reportLineage() {
|
|
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
cwd: repoRoot,
|
|
});
|
|
const generatedAt = new Date().toISOString();
|
|
const gitRevision = stdout.trim();
|
|
return {
|
|
runId: randomUUID(),
|
|
gitRevision,
|
|
generatedAt,
|
|
contentLineage: {
|
|
generator: 'scripts/computer-use/real-model.mjs',
|
|
gitRevision,
|
|
generatedAt,
|
|
},
|
|
};
|
|
}
|
|
|
|
async function reservePort() {
|
|
const server = createServer();
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', resolve);
|
|
});
|
|
const address = server.address();
|
|
const port = typeof address === 'object' && address ? address.port : 0;
|
|
await new Promise((resolve) => server.close(resolve));
|
|
if (!port) throw new Error('failed to reserve loopback port');
|
|
return port;
|
|
}
|
|
|
|
async function copyProfileFile(name, workspace) {
|
|
await cp(join(sourceWorkspace, name), join(workspace, name), {
|
|
errorOnExist: true,
|
|
});
|
|
}
|
|
|
|
async function copyProfileFileIfPresent(name, workspace) {
|
|
try {
|
|
await copyProfileFile(name, workspace);
|
|
} catch (error) {
|
|
if (error && error.code === 'ENOENT') return;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function prepareProviderProfile(workspace) {
|
|
if (!providerOverride) {
|
|
await copyProfileFile('connection-catalog.json', workspace);
|
|
await Promise.all([
|
|
copyProfileFileIfPresent('credential-vault.json', workspace),
|
|
copyProfileFileIfPresent('credentials.json', workspace),
|
|
copyProfileFile('settings.json', workspace),
|
|
]);
|
|
return;
|
|
}
|
|
if (providerOverride !== 'openai') {
|
|
throw new Error(`unsupported MAKA_CU_PROVIDER ${providerOverride}`);
|
|
}
|
|
await copyProfileFile('settings.json', workspace);
|
|
const modelId = process.env.MAKA_CU_OPENAI_MODEL ?? 'gpt-5.4';
|
|
const capability = await resolveStorageRoot({ path: workspace, kind: 'interactive' });
|
|
const owner = await tryAcquireInteractiveRootOwner(capability);
|
|
if (!owner) throw new Error('Unable to acquire the Computer Use catalog root');
|
|
try {
|
|
const stores = await openInteractiveRuntimePolicyStoresForWrite(owner.lease);
|
|
const created = await stores.connectionCatalog.create({
|
|
expectedCatalogRevision: 0,
|
|
connection: {
|
|
slug: 'cu-real-openai',
|
|
name: 'Computer Use real-model OpenAI',
|
|
providerType: 'openai',
|
|
baseUrl: process.env.MAKA_CU_OPENAI_BASE_URL ?? 'http://127.0.0.1:8538/v1',
|
|
enabled: true,
|
|
enabledModelIds: [modelId],
|
|
},
|
|
});
|
|
if (created.kind !== 'committed') {
|
|
throw new Error(`Computer Use catalog create failed: ${created.kind}`);
|
|
}
|
|
const connection = created.snapshot.connections.find((item) => item.slug === 'cu-real-openai');
|
|
if (!connection) throw new Error('Computer Use catalog create omitted cu-real-openai');
|
|
const vault = await stores.credentialVault.set({
|
|
locator: {
|
|
scope: 'connection',
|
|
connectionId: connection.connectionId,
|
|
kind: 'api_key',
|
|
},
|
|
expected: null,
|
|
secret: process.env.MAKA_CU_OPENAI_API_KEY ?? 'local-bridge',
|
|
});
|
|
if (vault.kind !== 'committed') {
|
|
throw new Error(`Computer Use vault set failed: ${vault.kind}`);
|
|
}
|
|
const defaulted = await stores.connectionCatalog.setDefaultTarget({
|
|
expectedCatalogRevision: created.snapshot.revision,
|
|
target: { connectionId: connection.connectionId, modelId },
|
|
});
|
|
if (defaulted.kind !== 'committed') {
|
|
throw new Error(`Computer Use default target failed: ${defaulted.kind}`);
|
|
}
|
|
} finally {
|
|
await owner.close();
|
|
}
|
|
}
|
|
|
|
async function waitForLine(child, marker, timeout) {
|
|
return new Promise((resolve, reject) => {
|
|
let stdout = '';
|
|
const timer = setTimeout(() => reject(new Error(`timed out waiting for ${marker}`)), timeout);
|
|
const onData = (chunk) => {
|
|
stdout += String(chunk);
|
|
process.stdout.write(chunk);
|
|
const matched =
|
|
marker === 'CU_FIXTURE_READY'
|
|
? /^CU_FIXTURE_READY\s+\d+\s*$/m.test(stdout)
|
|
: stdout.includes(marker);
|
|
if (matched) {
|
|
clearTimeout(timer);
|
|
child.stdout.off('data', onData);
|
|
resolve(stdout);
|
|
}
|
|
};
|
|
child.stdout.on('data', onData);
|
|
child.stderr.on('data', (chunk) => process.stderr.write(chunk));
|
|
child.once('error', reject);
|
|
child.once('exit', (code, signal) => {
|
|
clearTimeout(timer);
|
|
reject(new Error(`fixture exited before ready: ${signal ?? `code ${code}`}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
export function parseFixtureReady(text, childPid) {
|
|
const matches =
|
|
typeof text === 'string' ? [...text.matchAll(/^CU_FIXTURE_READY\s+(\d+)\s*$/gm)] : [];
|
|
if (matches.length !== 1) {
|
|
throw new Error('fixture READY identity is missing or ambiguous');
|
|
}
|
|
const readyPid = Number(matches[0][1]);
|
|
if (!Number.isInteger(childPid) || childPid <= 0 || readyPid !== childPid) {
|
|
throw new Error(`fixture READY pid ${readyPid} does not match launcher child pid ${childPid}`);
|
|
}
|
|
return readyPid;
|
|
}
|
|
|
|
export function parseTargetEvidence(text) {
|
|
if (typeof text !== 'string') return undefined;
|
|
const candidates = [text];
|
|
for (const markerText of ['Fresh observation:\n', 'Fresh observation: ']) {
|
|
const marker = text.lastIndexOf(markerText);
|
|
if (marker >= 0) candidates.push(text.slice(marker + markerText.length));
|
|
}
|
|
for (const candidate of candidates.reverse()) {
|
|
try {
|
|
const value = JSON.parse(candidate);
|
|
if (
|
|
value &&
|
|
typeof value === 'object' &&
|
|
Number.isInteger(value.pid) &&
|
|
Number.isInteger(value.window_id)
|
|
) {
|
|
return {
|
|
...(typeof value.observation_id === 'string'
|
|
? { observationId: value.observation_id }
|
|
: {}),
|
|
pid: value.pid,
|
|
windowId: value.window_id,
|
|
};
|
|
}
|
|
} catch {
|
|
// Continue to the next privacy-safe projection candidate.
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function actionRecords(events) {
|
|
const starts = new Map();
|
|
const records = [];
|
|
for (const event of events) {
|
|
if (event.type === 'tool_start' && event.toolName === 'maka_computer') {
|
|
starts.set(event.toolUseId, event);
|
|
}
|
|
if (event.type === 'tool_result' && starts.has(event.toolUseId)) {
|
|
const start = starts.get(event.toolUseId);
|
|
const text = event.content?.kind === 'text' ? event.content.text : undefined;
|
|
const resultCode =
|
|
typeof text === 'string'
|
|
? text.match(/\bfailed:\s*([a-z][a-z0-9_]{1,63})\b/i)?.[1]
|
|
: undefined;
|
|
const target = parseTargetEvidence(text);
|
|
records.push(
|
|
sanitizeCuActionRecord({
|
|
action: start.args,
|
|
toolCallId: event.toolUseId,
|
|
sourceObservationId: start.args?.observation_id,
|
|
resultObservationId: target?.observationId,
|
|
targetPid: target?.pid,
|
|
targetWindowId: target?.windowId,
|
|
durationMs: event.durationMs,
|
|
text,
|
|
success: event.isError === false && !resultCode,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
return records;
|
|
}
|
|
|
|
export async function waitForTraceFlush(path, expectedToolCallIds, timeoutMs = 2_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
const current = await readFile(path, 'utf8').catch((error) => {
|
|
if (error?.code === 'ENOENT') return '';
|
|
throw error;
|
|
});
|
|
const traces = [];
|
|
let incompleteTrace = false;
|
|
for (const line of current.split('\n').filter(Boolean)) {
|
|
try {
|
|
traces.push(JSON.parse(line));
|
|
} catch (error) {
|
|
if (!(error instanceof SyntaxError)) throw error;
|
|
incompleteTrace = true;
|
|
}
|
|
}
|
|
const observedToolCallIds = new Set(
|
|
traces.filter((trace) => trace.type === 'dispatch').map((trace) => trace.toolCallId),
|
|
);
|
|
if (
|
|
!incompleteTrace &&
|
|
expectedToolCallIds.every((toolCallId) => observedToolCallIds.has(toolCallId))
|
|
) {
|
|
return traces;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error(
|
|
`timed out waiting for Computer Use dispatch traces: ${expectedToolCallIds.join(',')}`,
|
|
);
|
|
}
|
|
|
|
export async function discoverFixtureIdentity(
|
|
fixturePid,
|
|
windowSpecs,
|
|
{ listApps, timeoutMs = 2_000, pollIntervalMs = 50 } = {},
|
|
) {
|
|
if (!Number.isInteger(fixturePid) || fixturePid <= 0) {
|
|
throw new Error('fixture discovery requires a valid launcher-owned pid');
|
|
}
|
|
if (typeof listApps !== 'function') {
|
|
throw new Error('fixture discovery requires an independent window lister');
|
|
}
|
|
const expectedTitles = windowSpecs?.map((window) => window.title);
|
|
if (
|
|
!Array.isArray(expectedTitles) ||
|
|
expectedTitles.length === 0 ||
|
|
expectedTitles.some((title) => typeof title !== 'string' || title.length === 0) ||
|
|
new Set(expectedTitles).size !== expectedTitles.length
|
|
) {
|
|
throw new Error('fixture discovery requires unique expected window titles');
|
|
}
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
let lastFailure = 'fixture app was not found';
|
|
while (Date.now() < deadline) {
|
|
const apps = await listApps();
|
|
const fixtureApps = Array.isArray(apps)
|
|
? apps.filter((app) => Number(app?.pid) === fixturePid)
|
|
: [];
|
|
if (fixtureApps.length === 1) {
|
|
const windows = Array.isArray(fixtureApps[0].windows) ? fixtureApps[0].windows : [];
|
|
const windowIds = [];
|
|
let complete = true;
|
|
for (const title of expectedTitles) {
|
|
const matches = windows.filter(
|
|
(window) =>
|
|
window?.title === title && Number.isInteger(window?.windowId) && window.windowId > 0,
|
|
);
|
|
if (matches.length !== 1) {
|
|
complete = false;
|
|
lastFailure = `expected one fixture window "${title}", got ${matches.length}`;
|
|
break;
|
|
}
|
|
windowIds.push(matches[0].windowId);
|
|
}
|
|
if (complete && new Set(windowIds).size === windowIds.length) {
|
|
return {
|
|
instances: [
|
|
{
|
|
pid: fixturePid,
|
|
windowIds,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (complete) lastFailure = 'fixture discovery returned duplicate window ids';
|
|
} else {
|
|
lastFailure = `expected one fixture app for pid ${fixturePid}, got ${fixtureApps.length}`;
|
|
}
|
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, pollIntervalMs));
|
|
}
|
|
throw new Error(`fixture identity discovery failed: ${lastFailure}`);
|
|
}
|
|
|
|
async function discoverLauncherFixtureIdentity(fixturePid, windowSpecs) {
|
|
const [{ createMakaCuBackend }, manifestText] = await Promise.all([
|
|
import('../../packages/computer-use/dist/index.js'),
|
|
readFile(join(repoRoot, 'apps', 'desktop', 'bundled-tools.json'), 'utf8'),
|
|
]);
|
|
const manifest = JSON.parse(manifestText);
|
|
const expectedBinarySha256 = manifest?.makaCu?.binarySha256;
|
|
if (typeof expectedBinarySha256 !== 'string' || !/^[a-f0-9]{64}$/.test(expectedBinarySha256)) {
|
|
throw new Error('fixture discovery cannot verify bundled maka-cu identity');
|
|
}
|
|
const backend = createMakaCuBackend({
|
|
binaryPath: join(repoRoot, 'apps', 'desktop', 'resources', 'bin', 'maka-cu'),
|
|
expectedBinarySha256,
|
|
timeoutMs: 10_000,
|
|
});
|
|
try {
|
|
return await discoverFixtureIdentity(fixturePid, windowSpecs, {
|
|
listApps: () => backend.listApps(new AbortController().signal),
|
|
timeoutMs: 5_000,
|
|
});
|
|
} finally {
|
|
backend.dispose();
|
|
}
|
|
}
|
|
|
|
function safeEvent(event) {
|
|
if (event.type === 'tool_start') {
|
|
const safeToolName =
|
|
event.toolName === 'tool_search' || event.toolName === 'maka_computer'
|
|
? event.toolName
|
|
: 'other';
|
|
return {
|
|
type: event.type,
|
|
toolName: safeToolName,
|
|
...(event.toolName === 'maka_computer'
|
|
? { actionType: event.args?.action ?? 'unknown' }
|
|
: {}),
|
|
ts: event.ts,
|
|
};
|
|
}
|
|
if (event.type === 'tool_result') {
|
|
return {
|
|
type: event.type,
|
|
isError: event.isError,
|
|
durationMs: event.durationMs,
|
|
ts: event.ts,
|
|
};
|
|
}
|
|
if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') {
|
|
const safeCode = [event.code, event.reason].find(
|
|
(value) => typeof value === 'string' && /^[a-zA-Z][a-zA-Z0-9_.-]{0,63}$/.test(value),
|
|
);
|
|
return {
|
|
type: event.type,
|
|
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
...(safeCode ? { code: safeCode } : {}),
|
|
ts: event.ts,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function safeFailureMetadata(message) {
|
|
if (typeof message !== 'string') return undefined;
|
|
const status = message.match(/\b([45]\d\d)\b/)?.[1];
|
|
const errorName = message.match(/\b([A-Z][A-Za-z]+Error)\b/)?.[1];
|
|
const providerType = message
|
|
.match(
|
|
/\b(api_error|authentication_error|billing_error|invalid_request_error|overloaded_error|permission_error|rate_limit_error)\b/i,
|
|
)?.[1]
|
|
?.toLowerCase();
|
|
const result = {
|
|
...(status ? { httpStatus: Number(status) } : {}),
|
|
...(errorName ? { errorName } : {}),
|
|
...(providerType ? { providerErrorType: providerType } : {}),
|
|
};
|
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
}
|
|
|
|
async function run() {
|
|
const lineage = await reportLineage();
|
|
const userData = await mkdtemp(join(tmpdir(), 'maka-cu-real-model-'));
|
|
const workspace = join(userData, 'workspaces', 'default');
|
|
const tracePath = join(userData, 'computer-use-trace.jsonl');
|
|
const fixturePort = await reservePort();
|
|
const electronBinary = join(repoRoot, 'node_modules', '.bin', 'electron');
|
|
let fixture;
|
|
let desktop;
|
|
let fixtureBrowser;
|
|
try {
|
|
await mkdir(workspace, { recursive: true });
|
|
await mkdir(dirname(reportPath), { recursive: true });
|
|
await prepareProviderProfile(workspace);
|
|
|
|
fixture = spawn(
|
|
electronBinary,
|
|
[
|
|
`--remote-debugging-port=${fixturePort}`,
|
|
'--remote-allow-origins=*',
|
|
join(here, 'real-model-fixture.mjs'),
|
|
],
|
|
{
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
MAKA_CU_E2E_SCENARIO: scenario.id,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
},
|
|
);
|
|
const readyOutput = await waitForLine(fixture, 'CU_FIXTURE_READY', 30_000);
|
|
const fixturePid = parseFixtureReady(readyOutput, fixture.pid);
|
|
const fixtureIdentity = await discoverLauncherFixtureIdentity(
|
|
fixturePid,
|
|
activeWindowSpecs(scenario),
|
|
);
|
|
|
|
desktop = await electron.launch({
|
|
args: ['apps/desktop'],
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
MAKA_CU_REAL_MODEL_E2E: '1',
|
|
MAKA_E2E_USER_DATA_DIR: userData,
|
|
MAKA_CU_REAL_MODEL_POLICY: JSON.stringify({
|
|
allowedActions: scenario.allowedActions,
|
|
maxTotalActions: scenario.maxTotalActions,
|
|
maxActionCounts:
|
|
scenario.maxActionCounts ??
|
|
Object.fromEntries(
|
|
scenario.allowedActions.map((action) => [action, scenario.maxTotalActions]),
|
|
),
|
|
allowedApps: activeWindowSpecs(scenario).map((window) => window.title),
|
|
}),
|
|
MAKA_CU_REAL_MODEL_TRACE: tracePath,
|
|
},
|
|
timeout: 30_000,
|
|
});
|
|
const page = await desktop.firstWindow();
|
|
await page.waitForFunction(() => Boolean(window.maka?.sessions));
|
|
|
|
const runResult = await page.evaluate(
|
|
async ({ prompt, timeout }) => {
|
|
const connections = await window.maka.connections.list();
|
|
const defaultSlug = await window.maka.connections.getDefault();
|
|
const connection = connections.find((entry) => entry.slug === defaultSlug);
|
|
if (!connection?.defaultModel) {
|
|
throw new Error('isolated profile has no ready default model');
|
|
}
|
|
const session = await window.maka.sessions.create({
|
|
backend: 'ai-sdk',
|
|
llmConnectionSlug: connection.slug,
|
|
model: connection.defaultModel,
|
|
permissionMode: 'bypass',
|
|
name: 'Computer Use real-model E2E',
|
|
labels: ['computer-use', 'real-model-e2e'],
|
|
});
|
|
const events = [];
|
|
const terminal = new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => reject(new Error('real-model turn timed out')), timeout);
|
|
const unsubscribe = window.maka.sessions.subscribeEvents(session.id, (event) => {
|
|
events.push(event);
|
|
if (event.type === 'complete' || event.type === 'abort' || event.type === 'error') {
|
|
clearTimeout(timer);
|
|
unsubscribe();
|
|
resolve(event);
|
|
}
|
|
});
|
|
});
|
|
const turnId = crypto.randomUUID();
|
|
await window.maka.sessions.send(session.id, {
|
|
type: 'send',
|
|
turnId,
|
|
text: prompt,
|
|
});
|
|
const terminalEvent = await terminal;
|
|
return {
|
|
connection: {
|
|
slug: connection.slug,
|
|
providerType: connection.providerType,
|
|
model: connection.defaultModel,
|
|
},
|
|
sessionId: session.id,
|
|
turnId,
|
|
events,
|
|
terminalEvent,
|
|
};
|
|
},
|
|
{ prompt: runPrompt, timeout: timeoutMs },
|
|
);
|
|
|
|
fixtureBrowser = await chromium.connectOverCDP(`http://127.0.0.1:${fixturePort}`);
|
|
const fixturePages = fixtureBrowser.contexts().flatMap((context) => context.pages());
|
|
const fixtureState = {};
|
|
for (const windowSpec of activeWindowSpecs(scenario)) {
|
|
const pageForTitle = await Promise.all(
|
|
fixturePages.map(async (candidate) => ({
|
|
candidate,
|
|
title: await candidate.title(),
|
|
})),
|
|
).then((entries) => entries.find((entry) => entry.title === windowSpec.title)?.candidate);
|
|
if (!pageForTitle) throw new Error(`missing fixture page ${windowSpec.title}`);
|
|
fixtureState[windowSpec.id] = await pageForTitle.evaluate(
|
|
() => globalThis.__makaCuFixtureState?.() ?? null,
|
|
);
|
|
}
|
|
const evaluation = evaluateCuE2eScenarioState(scenario, fixtureState);
|
|
const events = runResult.events.map(safeEvent).filter(Boolean);
|
|
const rawActions = actionRecords(runResult.events);
|
|
const expectedDispatchToolCallIds = rawActions
|
|
.filter(
|
|
(action) =>
|
|
action.success === true &&
|
|
!['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action.type),
|
|
)
|
|
.map((action) => action.toolCallId)
|
|
.filter((toolCallId) => typeof toolCallId === 'string');
|
|
const driverTraces = await waitForTraceFlush(tracePath, expectedDispatchToolCallIds);
|
|
const actions = bindActionTargets(rawActions, driverTraces, fixtureIdentity);
|
|
const runStore = createSqliteAgentRunStore(workspace);
|
|
await runStore.ready?.();
|
|
let runHeader;
|
|
try {
|
|
runHeader = await waitForRunHeader(runStore, runResult.sessionId, runResult.turnId);
|
|
} finally {
|
|
runStore.close?.();
|
|
}
|
|
const qualifyingActions = actions.filter(
|
|
(action) =>
|
|
action.success === true &&
|
|
action.targetOwned === true &&
|
|
scenario.allowedActions.includes(action.type),
|
|
);
|
|
const actionCounts = Object.fromEntries(
|
|
actions.reduce((counts, action) => {
|
|
counts.set(action.type, (counts.get(action.type) ?? 0) + 1);
|
|
return counts;
|
|
}, new Map()),
|
|
);
|
|
const qualifyingActionCounts = Object.fromEntries(
|
|
scenario.allowedActions.map((action) => [
|
|
action,
|
|
qualifyingActions.filter((record) => record.type === action).length,
|
|
]),
|
|
);
|
|
const minimumActionsPassed = Object.entries(scenario.minimumActionCounts ?? {}).every(
|
|
([action, minimum]) => (qualifyingActionCounts[action] ?? 0) >= minimum,
|
|
);
|
|
const terminalPassed =
|
|
runResult.terminalEvent.type === 'complete' &&
|
|
runResult.terminalEvent.stopReason === 'end_turn';
|
|
const actionsWithinBudget =
|
|
actions.length <= scenario.maxTotalActions &&
|
|
actions.every((action) => scenario.allowedActions.includes(action.type)) &&
|
|
Object.entries(scenario.maxActionCounts ?? {}).every(
|
|
([action, maximum]) => actions.filter((record) => record.type === action).length <= maximum,
|
|
);
|
|
const dispatchPathPassed = requiredDispatchPathPassed(scenario, driverTraces);
|
|
const ownershipPassed = allActionTargetsOwned(actions);
|
|
const localChecksPassed =
|
|
terminalPassed &&
|
|
minimumActionsPassed &&
|
|
actionsWithinBudget &&
|
|
dispatchPathPassed &&
|
|
ownershipPassed &&
|
|
evaluation.pass;
|
|
const reportInput = {
|
|
schemaVersion: 1,
|
|
...lineage,
|
|
evidenceClass: 'real-runtime',
|
|
policyMode: 'bypassed',
|
|
qualificationEligible: true,
|
|
toolExposure: 'direct-e2e',
|
|
scenarioId: scenario.id,
|
|
producer: 'computer-use/real-model',
|
|
transportClass: 'live-network',
|
|
provider: runResult.connection.providerType,
|
|
model: runResult.connection.model,
|
|
fixtureIdentity,
|
|
terminal: safeEvent(runResult.terminalEvent),
|
|
run: runHeader
|
|
? {
|
|
status: runHeader.status,
|
|
failureClass: runHeader.failureClass,
|
|
failure: safeFailureMetadata(runHeader.failureMessage),
|
|
durationMs:
|
|
runHeader.completedAt !== undefined
|
|
? Math.max(0, runHeader.completedAt - runHeader.createdAt)
|
|
: undefined,
|
|
}
|
|
: undefined,
|
|
actionAttempts: actions.length,
|
|
actionCount: actions.length,
|
|
actionCounts,
|
|
minimumActionsPassed,
|
|
actionsWithinBudget,
|
|
dispatchPathPassed,
|
|
actions,
|
|
fixtureState,
|
|
expectedState: evaluation.expected,
|
|
forbiddenEffects: {
|
|
status: evaluation.forbidden.every((entry) => entry.pass) ? 'pass' : 'fail',
|
|
violations: evaluation.forbidden.filter((entry) => !entry.pass),
|
|
},
|
|
status: localChecksPassed
|
|
? 'pass'
|
|
: terminalPassed && minimumActionsPassed
|
|
? 'fail'
|
|
: 'inconclusive',
|
|
traces: events.map(sanitizeCuTrace).filter(Boolean),
|
|
driverTraces: driverTraces.map(sanitizeCuTrace).filter(Boolean),
|
|
};
|
|
const provider = {
|
|
id: runResult.connection.providerType,
|
|
producer: 'computer-use/real-model',
|
|
model: runResult.connection.model,
|
|
};
|
|
const provisionalReport = sanitizeCuReport(reportInput);
|
|
const qualificationErrors = validateRealReport(
|
|
{ ...provisionalReport, status: 'pass' },
|
|
provider,
|
|
scenario,
|
|
);
|
|
const report = sanitizeCuReport({
|
|
...reportInput,
|
|
status: qualificationErrors.length === 0 ? 'pass' : 'fail',
|
|
});
|
|
const validationErrors = validateRealReport(report, provider, scenario);
|
|
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, {
|
|
flag: 'wx',
|
|
mode: 0o600,
|
|
});
|
|
process.stdout.write(`Real-model Computer Use report: ${reportPath}\n`);
|
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
if (validationErrors.length > 0) {
|
|
process.stderr.write(
|
|
`Real-model Computer Use qualification failed: ${validationErrors.join('; ')}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
}
|
|
} finally {
|
|
await fixtureBrowser?.close().catch(() => {});
|
|
await desktop?.close().catch(() => {});
|
|
if (fixture && fixture.exitCode === null) fixture.kill('SIGTERM');
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
if (fixture && fixture.exitCode === null) fixture.kill('SIGKILL');
|
|
if (keepProfile) {
|
|
process.stderr.write(`Retained isolated debug profile: ${userData}\n`);
|
|
} else {
|
|
await rm(userData, { recursive: true, force: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
async function handleRunFailure(error) {
|
|
const lineage = await reportLineage().catch(() => undefined);
|
|
const failure = sanitizeCuReport({
|
|
schemaVersion: 1,
|
|
...lineage,
|
|
evidenceClass: 'real-runtime',
|
|
scenarioId: scenario.id,
|
|
producer: 'computer-use/real-model',
|
|
status: 'inconclusive',
|
|
terminal: { type: 'error' },
|
|
run: {
|
|
status: 'failed',
|
|
failureClass:
|
|
typeof error?.code === 'string'
|
|
? error.code
|
|
: error instanceof Error
|
|
? error.name
|
|
: 'unknown',
|
|
},
|
|
});
|
|
await mkdir(dirname(reportPath), { recursive: true }).catch(() => {});
|
|
await writeFile(reportPath, `${JSON.stringify(failure, null, 2)}\n`, {
|
|
flag: 'wx',
|
|
mode: 0o600,
|
|
}).catch(() => {});
|
|
console.error('Real-model Computer Use E2E failed');
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
function requiredDispatchPathPassed(scenario, traces) {
|
|
const mutationActions = scenario.allowedActions.filter(
|
|
(action) => !['list_apps', 'observe', 'screenshot', 'cursor_position', 'wait'].includes(action),
|
|
);
|
|
if (mutationActions.length === 0) return true;
|
|
return traces.some(
|
|
(trace) =>
|
|
trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'),
|
|
);
|
|
}
|
|
|
|
function fixtureOwnsTarget(fixtureIdentity, pid, windowId) {
|
|
return (
|
|
fixtureIdentity?.instances?.some(
|
|
(instance) => instance.pid === pid && instance.windowIds?.includes(windowId),
|
|
) === true
|
|
);
|
|
}
|
|
|
|
export function bindActionTargets(actions, traces, fixtureIdentity) {
|
|
const dispatches = traces.filter(
|
|
(trace) =>
|
|
trace.type === 'dispatch' && (trace.address === 'ax' || trace.address === 'semantic'),
|
|
);
|
|
const observationTargets = new Map(
|
|
actions.flatMap((action) =>
|
|
(action.type === 'observe' || action.type === 'screenshot') &&
|
|
typeof action.resultObservationId === 'string' &&
|
|
Number.isInteger(action.targetPid) &&
|
|
Number.isInteger(action.targetWindowId)
|
|
? [
|
|
[
|
|
action.resultObservationId,
|
|
{
|
|
pid: action.targetPid,
|
|
windowId: action.targetWindowId,
|
|
},
|
|
],
|
|
]
|
|
: [],
|
|
),
|
|
);
|
|
const consumed = new Set();
|
|
return actions.map((action) => {
|
|
if (['list_apps', 'wait', 'cursor_position'].includes(action.type)) {
|
|
return { ...action, targetOwned: false };
|
|
}
|
|
const index = dispatches.findIndex(
|
|
(trace, traceIndex) =>
|
|
!consumed.has(traceIndex) &&
|
|
trace.toolCallId === action.toolCallId &&
|
|
trace.actionType === action.type,
|
|
);
|
|
if (index >= 0) consumed.add(index);
|
|
const dispatch = index >= 0 ? dispatches[index] : undefined;
|
|
const sourceTarget =
|
|
typeof action.sourceObservationId === 'string'
|
|
? observationTargets.get(action.sourceObservationId)
|
|
: undefined;
|
|
const directObservationTarget =
|
|
(action.type === 'observe' || action.type === 'screenshot') &&
|
|
Number.isInteger(action.targetPid) &&
|
|
Number.isInteger(action.targetWindowId)
|
|
? { pid: action.targetPid, windowId: action.targetWindowId }
|
|
: undefined;
|
|
const target = dispatch
|
|
? { pid: dispatch.pid, windowId: dispatch.windowId }
|
|
: sourceTarget
|
|
? sourceTarget
|
|
: directObservationTarget;
|
|
return {
|
|
...action,
|
|
targetOwned: target ? fixtureOwnsTarget(fixtureIdentity, target.pid, target.windowId) : false,
|
|
...(target
|
|
? {
|
|
targetPid: target.pid,
|
|
targetWindowId: target.windowId,
|
|
}
|
|
: {}),
|
|
};
|
|
});
|
|
}
|
|
|
|
export function allActionTargetsOwned(actions) {
|
|
return actions.every(
|
|
(action) =>
|
|
['list_apps', 'wait', 'cursor_position'].includes(action.type) || action.targetOwned === true,
|
|
);
|
|
}
|
|
|
|
function activeWindowSpecs(scenario) {
|
|
const specs = new Map(scenario.fixtureSetup.windows.map((window) => [window.id, window]));
|
|
for (const transition of scenario.fixtureSetup.transitions ?? []) {
|
|
specs.delete(transition.removeWindowId);
|
|
specs.set(transition.addWindow.id, transition.addWindow);
|
|
}
|
|
return [...specs.values()];
|
|
}
|
|
|
|
async function waitForRunHeader(store, sessionId, turnId) {
|
|
const deadline = Date.now() + 2_000;
|
|
let latest;
|
|
do {
|
|
latest = (await store.listSessionRuns(sessionId)).find((entry) => entry.turnId === turnId);
|
|
if (
|
|
latest &&
|
|
latest.status !== 'created' &&
|
|
latest.status !== 'running' &&
|
|
latest.status !== 'waiting_for_user'
|
|
)
|
|
return latest;
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
} while (Date.now() < deadline);
|
|
return latest;
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
run().catch(handleRunFailure);
|
|
}
|