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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ Agent Computer Use Platform gives agents a real Linux desktop to work in, with s
- Runs agent tasks inside disposable Linux sessions instead of collapsing everything into a browser tab.
- Gives you one place to work with screenshots, shell commands, files, desktop input, and session state.
- Keeps operators in the loop with a live desktop view, structured receipts, and clear fallback modes.
- Exposes a `review_recording` summary for qemu `product` sessions and lets you explicitly export a sparse review bundle for later human review instead of recording default video.

## Default flow

The default happy path is now:

1. Start a session with default settings (`qemu` + `product`)
2. Wait for readiness
3. Submit a task
4. Watch `live_desktop_view` or the truthful screenshot fallback
5. Export the sparse review bundle if you want durable evidence for later review
6. Delete the session when done; only exported bundles survive session teardown

QEMU review recording is intentionally storage-first in v1: the durable artifact is a sparse review bundle (`review.json`, `timeline.jsonl`, deduplicated screenshots), not a continuous video capture.

That is the workflow agents should infer first. Advanced/debug controls still exist, but they are secondary.

## Quickstart

Expand Down
91 changes: 90 additions & 1 deletion apps/control-plane/src/server.desktop-app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { createServer } from 'node:http';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

Expand Down Expand Up @@ -54,6 +54,27 @@ async function startGuestServer() {
return;
}

if (req.method === 'POST' && url.pathname === '/api/storage/reclaim') {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(Buffer.from(chunk));
const body = JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>;
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
mode: body.mode ?? 'report',
candidate_count: 1,
candidates: [
{
path: '/tmp/inspectors/runtime/stale-session',
tier: 'runtime',
kind: 'legacy_runtime',
reason: 'legacy inspectors runtime directory without an active container reference',
},
],
reclaimed: body.mode === 'apply' ? ['/tmp/inspectors/runtime/stale-session'] : [],
}));
return;
}

res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found', path: url.pathname }));
});
Expand Down Expand Up @@ -152,3 +173,71 @@ test('qemu product session creation preserves desktop user metadata and live vie
await stopServers(controlPlane, guest.guestServer);
}
});

test('control-plane proxies storage reclaim requests', async () => {
const guest = await startGuestServer();
const controlPlane = await startControlPlaneServer(0, guest.baseUrl);
const baseUrl = `http://127.0.0.1:${(controlPlane.server.address() as { port: number }).port}`;

try {
const response = await fetch(`${baseUrl}/api/storage/reclaim`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ mode: 'apply' }),
});
assert.equal(response.status, 200);
const payload = await response.json() as {
mode: string;
candidate_count: number;
reclaimed: string[];
};
assert.equal(payload.mode, 'apply');
assert.equal(payload.candidate_count, 1);
assert.deepEqual(payload.reclaimed, ['/tmp/inspectors/runtime/stale-session']);
} finally {
await stopServers(controlPlane, guest.guestServer);
}
});

test('qemu product session creation can activate the desktop app when configured', async () => {
const activationDir = tempDir('acu-desktop-activate');
const activationScript = join(activationDir, 'activate.js');
const activationOutput = join(activationDir, 'activation.json');
writeFileSync(
activationScript,
`const fs = require('node:fs'); fs.writeFileSync(process.argv[2], JSON.stringify(process.argv.slice(3)));`,
);

const previousActivateBin = process.env.ACU_DESKTOP_ACTIVATE_BIN;
const previousActivateArgs = process.env.ACU_DESKTOP_ACTIVATE_ARGS_JSON;
process.env.ACU_DESKTOP_ACTIVATE_BIN = process.execPath;
process.env.ACU_DESKTOP_ACTIVATE_ARGS_JSON = JSON.stringify([activationScript, activationOutput]);

const guest = await startGuestServer();
const controlPlane = await startControlPlaneServer(0, guest.baseUrl);
const baseUrl = `http://127.0.0.1:${(controlPlane.server.address() as { port: number }).port}`;

try {
const response = await fetch(`${baseUrl}/api/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ provider: 'qemu', qemu_profile: 'product' }),
});
assert.equal(response.status, 201);
const activationArgs = JSON.parse(readFileSync(activationOutput, 'utf8')) as string[];
assert.deepEqual(activationArgs, ['--activate-desktop', '--session', 'qemu-product']);
} finally {
if (previousActivateBin === undefined) {
delete process.env.ACU_DESKTOP_ACTIVATE_BIN;
} else {
process.env.ACU_DESKTOP_ACTIVATE_BIN = previousActivateBin;
}
if (previousActivateArgs === undefined) {
delete process.env.ACU_DESKTOP_ACTIVATE_ARGS_JSON;
} else {
process.env.ACU_DESKTOP_ACTIVATE_ARGS_JSON = previousActivateArgs;
}
await stopServers(controlPlane, guest.guestServer);
rmSync(activationDir, { recursive: true, force: true });
}
});
89 changes: 89 additions & 0 deletions apps/control-plane/src/server.live-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ function sessionRecord(id: string, overrides: Record<string, unknown> = {}) {
runtime_base_url: 'http://127.0.0.1:4001',
viewer_url: null,
live_desktop_view: null,
review_recording: {
mode: 'sparse_timeline',
status: 'active',
retention: 'ephemeral_until_export',
event_count: 1,
screenshot_count: 0,
approx_bytes: 128,
last_captured_at: new Date().toISOString(),
exportable: true,
exported_bundle: null,
postmortem_retained_until: null,
reason: null,
},
bridge_status: 'runtime_ready',
readiness_state: 'runtime_ready',
bridge_error: null,
Expand Down Expand Up @@ -135,6 +148,19 @@ async function startHarness(): Promise<Harness> {
provider: 'qemu',
qemu_profile: 'regression',
viewer_url: viewerUrl,
review_recording: {
mode: 'unavailable',
status: 'unavailable',
retention: 'ephemeral_until_export',
event_count: 0,
screenshot_count: 0,
approx_bytes: 0,
last_captured_at: null,
exportable: false,
exported_bundle: null,
postmortem_retained_until: null,
reason: 'review recording is available only for qemu product sessions in v1',
},
}),
}));
return;
Expand All @@ -148,10 +174,51 @@ async function startHarness(): Promise<Harness> {
viewer_url: null,
display: ':90',
capabilities: ['screenshot'],
review_recording: {
mode: 'unavailable',
status: 'unavailable',
retention: 'ephemeral_until_export',
event_count: 0,
screenshot_count: 0,
approx_bytes: 0,
last_captured_at: null,
exportable: false,
exported_bundle: null,
postmortem_retained_until: null,
reason: 'review recording is available only for qemu product sessions in v1',
},
}),
}));
return;
}
if (req.method === 'POST' && url.pathname === '/api/sessions/qemu-product/review/export') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
bundle: {
kind: 'review_bundle',
path: 'artifacts/exports/qemu-product-review',
mime_type: null,
},
review_recording: {
mode: 'sparse_timeline',
status: 'exported',
retention: 'ephemeral_until_export',
event_count: 4,
screenshot_count: 2,
approx_bytes: 512,
last_captured_at: new Date().toISOString(),
exportable: true,
exported_bundle: {
kind: 'review_bundle',
path: 'artifacts/exports/qemu-product-review',
mime_type: null,
},
postmortem_retained_until: null,
reason: null,
},
}));
return;
}
if (req.method === 'GET' && url.pathname === '/api/sessions/missing') {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'session not found' }));
Expand Down Expand Up @@ -203,16 +270,20 @@ test('session metadata exposes truthful live_desktop_view modes', async () => {
assert.equal(qemuProduct.session.live_desktop_view.canonical_url, '/api/sessions/qemu-product/live-view/');
assert.equal(qemuProduct.session.live_desktop_view.debug_url, harness.viewerUrl);
assert.equal(qemuProduct.session.live_desktop_view.matches_action_plane, true);
assert.equal(qemuProduct.session.review_recording.mode, 'sparse_timeline');
assert.equal(qemuProduct.session.review_recording.exportable, true);

const qemuRegression = await fetch(`${harness.baseUrl}/api/sessions/qemu-regression`).then((res) => res.json()) as { session: any };
assert.equal(qemuRegression.session.live_desktop_view.mode, 'screenshot_poll');
assert.equal(qemuRegression.session.live_desktop_view.canonical_url, '/api/sessions/qemu-regression/screenshot');
assert.equal(qemuRegression.session.live_desktop_view.debug_url, harness.viewerUrl);
assert.equal(qemuRegression.session.review_recording.mode, 'unavailable');

const xvfb = await fetch(`${harness.baseUrl}/api/sessions/xvfb`).then((res) => res.json()) as { session: any };
assert.equal(xvfb.session.live_desktop_view.mode, 'screenshot_poll');
assert.equal(xvfb.session.live_desktop_view.canonical_url, '/api/sessions/xvfb/screenshot');
assert.match(String(xvfb.session.live_desktop_view.reason), /screenshot fallback/i);
assert.equal(xvfb.session.review_recording.mode, 'unavailable');
} finally {
await stopHarness(harness);
}
Expand Down Expand Up @@ -305,3 +376,21 @@ test('live-view websocket upgrades proxy to the upstream viewer', async () => {
await stopHarness(harness);
}
});

test('review export route proxies durable review bundle metadata', async () => {
const harness = await startHarness();
try {
const response = await fetch(`${harness.baseUrl}/api/sessions/qemu-product/review/export`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({}),
});
assert.equal(response.status, 200);
const payload = await response.json() as { bundle: { kind: string; path: string }; review_recording: { status: string } };
assert.equal(payload.bundle.kind, 'review_bundle');
assert.equal(payload.bundle.path, 'artifacts/exports/qemu-product-review');
assert.equal(payload.review_recording.status, 'exported');
} finally {
await stopHarness(harness);
}
});
Loading
Loading