diff --git a/docs/NODE_AGENT_API_OPERATIONS.md b/docs/NODE_AGENT_API_OPERATIONS.md
index 8e79c4b..b35698a 100644
--- a/docs/NODE_AGENT_API_OPERATIONS.md
+++ b/docs/NODE_AGENT_API_OPERATIONS.md
@@ -82,6 +82,10 @@ curl --fail --max-time 5 http://127.0.0.1:18770/health
## 停止与重启
+VNC 第一阶段改动和测试记录见 [连接预热与查看器复用](NODE_AGENT_VNC_PHASE_ONE.md)。内嵌查看器产物需在 API 重载前构建;保留正在使用的状态目录和密钥。
+
+2026-10-07的网页会话与VNC性能改动已通过现有LaunchAgent重载生效,保留原状态目录、密钥及13个已有会话。更新后刷新网页并重新点击观看;详细结果与尚未达到的预算见[性能验证](NODE_AGENT_API_PERFORMANCE.md)。
+
先结束或取消当前 API 回合。停止本次后台实例:
```sh
diff --git a/docs/NODE_AGENT_API_PERFORMANCE.md b/docs/NODE_AGENT_API_PERFORMANCE.md
new file mode 100644
index 0000000..48c8167
--- /dev/null
+++ b/docs/NODE_AGENT_API_PERFORMANCE.md
@@ -0,0 +1,57 @@
+# Node Agent API 性能优化与验证
+
+日期:2026-10-07。本次针对网页会话提交和 VNC 打开过程。没有更换模型、端点或推理强度,没有迁移会话格式,没有重新打包 GrokNode App 或重建容器。
+
+本文保留首轮优化的测量记录。后续查看器预热、复用和打包后的实现及最新结果见 [VNC 第一阶段](NODE_AGENT_VNC_PHASE_ONE.md)。
+
+## 已实施
+
+- 桌面解析:一次 fresh Docker inspect 加一次批量容器执行完成 roster、状态和归属验证;使用 node:http 避免短进程中的 fetch 初始化成本。正常桌面不重复 ensure。
+- 会话提交:存活线程与当前容器、启动时间、桌面分配和 Harness 连接身份一致时走快速路径;变化或冷恢复时保留完整验证。
+- 查看器:issue/open/upgrade 保留各自授权和实时身份核对;静态内容使用 private/no-cache 与内容 ETag,新 viewer 授权仍可条件复用。动态授权配置不缓存。
+- 持久化:状态与通知一次原子提交;普通 delta 最多25毫秒或64条一批,保留每个事件与游标顺序。完成、审批、取消、断线、读取和关停按明确边界 drain。
+- 网页:文字与日志按帧绘制,刷新请求合并;旧快照不再覆盖正在输出的文本,切换会话仍清理旧状态。
+
+## 真实环境观察
+
+| 操作 | 之前 | 优化后 | 范围 |
+| --- | --- | --- | --- |
+| 已运行桌面的环境查询 |3982/4442毫秒|445/437/442毫秒|不同时间的低频只读样本;每次1 inspect+1 exec |
+| 点击观看到可见桌面 canvas |16907毫秒|8011/5601/5642毫秒|更新后第一次及两次重复打开;连接成功、图像有变化 |
+
+更新后重复打开约5.6秒,比之前的单次16.9秒观察缩短约三分之二。样本少、时间与桌面选择可能不同,不能解释为严格同条件 p95 或模型推理加速。真实观察采用同一点击至可见不透明 canvas 的计时方式,不等同于准确的首 framebuffer-update 事件;受控图案测试另行验证解码和首帧。
+
+**真实 VNC 尚未达到2–3秒的拟定预算。** 最后一次查看器共83个资源请求,HTTP/1.1下部分 SVG 等资源在浏览器排队约1.3–1.6秒,服务端等待约0.1–0.2秒,iframe加载约2.4秒。资源加载、校验和远端画面初始化仍占用时间;本轮不改画质或绕过身份检查。
+
+## 受控对比
+
+原始归档与候选使用相同测量工具,每组30个冷样本、100个热样本;产品源码哈希在测量前后保持一致并与交付代码匹配。runtime和RFB上游为合成 fixture,HTTP、Harness子进程、文件同步、SSE与Chromium解码/绘制是真实路径,不调用模型。
+
+| 热路径指标 | 原版中位数 | 候选中位数 |
+| --- | --- | --- |
+| HTTP提交到 Harness turn/start 写出 |55.0毫秒|32.2毫秒 |
+| HTTP接受响应 |121.5毫秒|95.8毫秒 |
+| 单条通知到 SSE 客户端 |20.4毫秒|47.3毫秒 |
+| 已知图案 noVNC 首帧 |94.8毫秒|83.8毫秒 |
+
+单条 SSE 多等待约27毫秒,主要是有意引入的最多25毫秒增量批次;突发输出获得明显写入收益。256个增量保留256个有序事件,仅4次磁盘提交。网页5秒内5000个增量合为500次文字写入,每个项目每帧至多一次,绘制延迟 p95 约16毫秒。
+
+**冷查看器 fixture 没有提速:**中位数210.9→245.5毫秒,p95 233.7→297.2毫秒。浏览器启动和认证发生在计时前,不能用启动时间解释该差异。未计算置信区间。自动化客户端发生过2个原版、4个候选冷组异常,原始尝试保留并以同配置补齐;不能将这些异常隐藏为产品成功。
+
+## 验证
+
+- API回归190/190。
+- 仓库检查:1057通过,129项按已有构建条件跳过,0失败;前端与源码类型检查通过。
+- 前端构建通过,保留已有动态导入提示。
+- 测量工具最终4/4;UI与Chromium17/17;查看器授权/缓存49/49;持久化19/19;事件集成92/92。
+- 独立审查通过,包含授权撤销、分配变化、原子提交、配额拒绝、fsync/rename故障、重放顺序和快照竞争。
+
+## 部署与恢复
+
+已通过原有 `com.groknode.node-agent-api` LaunchAgent 完成受控重载。重载前无活动API回合,重载后健康/认证正常,原密钥字节和13个既有会话ID保持一致。用户需要刷新网页并重新签发查看器授权;不需要重新创建会话。
+
+本机源码位于 `grok-node-agent-api-0-3` 工作副本,实际 `--state`、`--runtime-state` 仍引用 `grok-node-codex-lab/.lab`。不要将状态路径改成当前源码目录的默认 `.lab`。
+
+回退启动预先保留的旧API源码入口并沿用原状态目录;无数据格式迁移。不要用 `git reset --hard` 覆盖共享工作区,不要重建容器来更新这个Mac API。
+
+完整机器证据保存在本次私有 `.omo/evidence/node-agent-api-speed/` 下,包括来源哈希、原始尝试、测试日志和操作结果;密钥、实际会话内容和真实桌面图像不进入本文。测量工具说明见 [bench README](../tools/node-agent-api/bench/README.md),后台流程见[运行手册](NODE_AGENT_API_OPERATIONS.md)。
diff --git a/docs/NODE_AGENT_VNC_PHASE_ONE.md b/docs/NODE_AGENT_VNC_PHASE_ONE.md
new file mode 100644
index 0000000..f5ff2a6
--- /dev/null
+++ b/docs/NODE_AGENT_VNC_PHASE_ONE.md
@@ -0,0 +1,53 @@
+# VNC 第一阶段:连接预热和查看器复用
+
+日期:2026-10-07。范围为当前一个查看器的复用、只读预热、独立 viewer 授权、查看器打包和常驻只读校验通道。第二阶段的近期桌面池、同一连接上的接管/交还未实施。
+
+## 实现
+
+- 全部 viewer 使用独立 lane 和 HttpOnly Cookie,避免完整桌面授权相互覆盖。关闭单个 viewer 不影响其他 viewer。
+- 选中已有会话、读取运行环境后,等待 400ms 再预热当前桌面。预热仅观看已有桌面,不启动桌面,不替换已有控制租约。显式观看可复用完成首帧、仍有授权的当前查看器。
+- 切换会话、断开认证、过期和连接中断清理旧查看器;旧 iframe 消息和晚到的预热响应不改变新会话。
+- noVNC 1.6.0 的核心和内嵌入口打为一个约 191KB 的文件,包含原库许可证注释。保留默认画质 6、压缩 2。构建读取本机暂存的当前容器资源,不下载或安装依赖。
+- 常驻低权限 Docker exec 通道只有读取最新桌面快照这一项操作。每次读取最新 gateway 状态、分配和令牌摘要,前后检查容器 ID/StartedAt;不使用分配 TTL 缓存。通道崩溃、重启、超限或超时拒绝请求,runtime.close 清理进程。
+- 共享容器的保留连接每秒校验一次归属;变化或校验失败关闭连接。服务器仍执行只读输入过滤、控制租约、密钥撤销与过期检查。
+
+## 已有测试证据
+
+相关 API 回归:239/239 通过,无跳过。涵盖独立 lane、预热不准备桌面/不替换控制、过期/撤销、桌面重新分配、容器重启、进程清理、前端竞态和原会话功能。
+
+实际 Chromium + API + noVNC + 已知颜色 RFB 夹具完成 5 组。每组覆盖真实画面像素、复用无额外授权/连接、选择清理、旧消息隔离、预热保留控制、接管/交还和取消在途预热:
+
+| 场景 | 样本 | p50 | p95 |
+| --- | ---: | ---: | ---: |
+| 未预热首次打开 | 5 | 310ms | 315ms |
+| 预热后显示 | 5 | 1.5ms | 12ms |
+| 重复显示 | 5 | 0.3ms | 11ms |
+
+这些数值验证本地 UI/协议行为,夹具不包含真实 Docker 校验及远端桌面等待,不能代替真实首帧预算。
+
+真实只读桌面使用同一个运行中的 `:3`,独立临时 API 状态与密钥,没有模型请求、桌面输入或控制接管。基线和候选使用同一个 ego-browser TaskSpace:
+
+| 场景 | 样本 | p50 | p95 / 最大值 |
+| --- | ---: | ---: | ---: |
+| 第一阶段前,新连接 | 6 | 2.10s | 14.23s |
+| 第一阶段后,显式点击早于预热的新连接 | 10 | 1.84s | 3.10s |
+
+基线第一次 14.23s,其后五次 2.08–2.62s;未删去第一次。候选范围 0.91–3.10s。样本少、测试非随机交错,部分样本与回归测试重叠,不能作为稳定线上 p95 或严格速度提升百分比。
+
+候选另一次首次打开为 1.99s;iframe 资源请求从基线 83 个降为 1 个。隐藏预热约 2.07s 完成真实首帧;点击后同一个 iframe 约 2ms 完成 DOM 显示。另一重复显示测到下一次浏览器动画帧为 1.00s,无额外授权请求。ego 浏览器的 100 次连续动画帧测量超过单次求值的 15s 上限,保留该工具异常并刷新页面终止晚到执行,没有把它记为成功样本。
+
+真实画面计时基于完整真实 framebuffer update(排除伪矩形)、显示队列清空和动画帧;不使用 iframe load 或 WebSocket connected 代替首帧。未保存真实桌面像素。受控夹具另有已知像素断言和截图。
+
+## 全量检查和边界
+
+类型检查通过。全量 npm run check 为 1105 个通过、129 个原有条件跳过、1 个失败。唯一失败在未改动的 marketplace-d17-classnames:读取不到官方 App 中固定名称 `chunk-plugin-detail-view-tZskkHRA.js`。单独运行该测试仍复现,测试与验证脚本均未修改。没有删除、跳过或放宽此检查。
+
+第一阶段实现及相关功能验收已通过,真实首帧的尾部样本仍有 3.10s,尚不能宣布稳定达到 2–3 秒预算。用户已决定不做第二阶段:近期桌面池主要改善切换不同 Bot,同连接接管主要改善权限模式切换,都不能直接解决首次连接的剩余尾延迟。首开尾延迟单独分析 RFB 握手和浏览器绘制调度。
+
+原始证据:`.omo/evidence/vnc-phase-one-20261007/`,受控浏览器证据:`.omo/evidence/viewer-phase-one-sidecar/`。自动化工具失败和早期修正尝试均保留。
+
+## 本机部署验收
+
+已通过现有 LaunchAgent 重载 18770。首次 bootstrap 与旧任务退出发生时序冲突,待旧实例完全退出后相同配置加载成功。重载前无活动回合,重载后健康检查、原密钥和 13 个会话身份一致。
+
+正式网页选择已有会话后,只读隐藏预热收到 ready、connected 和 first-frame,iframe 为独立 lane 且只有一个资源;点击观看复用相同 iframe 并显示画面。没有新增正式会话、模型请求、控制接管、容器重建或 App 重打包。
diff --git a/tests/node-agent-api-web-ui-codex-contract.test.mjs b/tests/node-agent-api-web-ui-codex-contract.test.mjs
index a760fd5..11d9eec 100644
--- a/tests/node-agent-api-web-ui-codex-contract.test.mjs
+++ b/tests/node-agent-api-web-ui-codex-contract.test.mjs
@@ -309,7 +309,7 @@ if (process.env.NODE_AGENT_UI_BROWSER === '1') test('real Chromium UI contract:
capabilityMode = 'unknown'; await fill('#key', 'contract-test-key'); await click('#connect');
await waitFor("document.getElementById('bots').options.length === 3");
await page.locator('#bots').selectOption('bot_1'); await click('#attach');
- await waitFor("document.getElementById('thread').textContent === 'session_1'");
+ await waitFor("document.getElementById('thread').textContent === '当前会话' && document.querySelector('#sessions button[aria-current=\"true\"]')?.title === 'session_1'");
assert.equal(await evaluate("['send','cancel','view','control','clipboard-read','clipboard-write','actions-refresh','diff-refresh','import','export'].every(id=>document.getElementById(id).disabled)"), true);
await capture('unsupported-capabilities');
assert.equal(await evaluate('localStorage.length === 0 && sessionStorage.length === 0'), true);
diff --git a/tests/node-agent-api.test.mjs b/tests/node-agent-api.test.mjs
index 91ef97f..a7844e2 100644
--- a/tests/node-agent-api.test.mjs
+++ b/tests/node-agent-api.test.mjs
@@ -211,13 +211,13 @@ test('desktop authorization is single-use, bot-bound and server enforced', async
assert.equal(opened.status, 303);
const cookie = /nodeviewer=([A-Za-z0-9_-]+)/.exec(opened.headers.get('set-cookie'))[1];
assert.equal((await call('GET', '/desktop/open?ticket=' + ticket, { raw: true, headers: { authorization } })).status, 401);
- const mandatory = await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } });
+ const mandatory = await call('GET', '/desktop/lane/' + cookie + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } });
const settings = await mandatory.json();
const validateSettings = ajv.compile({ $ref: schemaId + '#/components/schemas/ViewerSettings' });
assert.ok(validateSettings(settings), JSON.stringify(validateSettings.errors));
assert.equal(settings.view_only, true);
- assert.equal(settings.path, 'desktop/socket');
- const html = await call('GET', '/desktop/vnc.html', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } });
+ assert.equal(settings.path, 'desktop/lane/' + cookie + '/socket');
+ const html = await call('GET', '/desktop/lane/' + cookie + '/vnc.html', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } });
assert.match(await html.text(), /#noVNC_control_bar/);
const scoped = issued[0];
const crossBot = await call('POST', '/v1/agents/sessions/' + sessions.b.id + '/desktop', { headers: { authorization: 'Bearer ' + scoped.key }, body: { mode: 'view' } });
@@ -226,7 +226,7 @@ test('desktop authorization is single-use, bot-bound and server enforced', async
});
async function driveViewer(cookie, messages) {
- const socket = new WebSocket(service.origin.replace('http:', 'ws:') + '/desktop/socket', { headers: { cookie: 'nodeviewer=' + cookie, origin: service.origin } });
+ const socket = new WebSocket(service.origin.replace('http:', 'ws:') + '/desktop/lane/' + cookie + '/socket', { headers: { cookie: 'nodeviewer=' + cookie, origin: service.origin } });
const closed = new Promise(resolve => { socket.once('close', resolve); socket.once('error', resolve); });
await new Promise((resolve, reject) => { socket.once('open', resolve); socket.once('error', reject); });
try {
@@ -264,7 +264,7 @@ test('control viewers can deliver input and hold a single exclusive lease', asyn
const ticket = new URL(control.data.url).searchParams.get('ticket');
const opened = await call('GET', '/desktop/open?ticket=' + ticket, { raw: true, headers: { authorization } });
const cookie = /nodeviewer=([A-Za-z0-9_-]+)/.exec(opened.headers.get('set-cookie'))[1];
- const settings = await (await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } })).json();
+ const settings = await (await call('GET', '/desktop/lane/' + cookie + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + cookie } })).json();
assert.equal(settings.view_only, false);
const busy = await call('POST', '/v1/agents/sessions/' + sessions.a.id + '/desktop', { headers: { authorization }, body: { mode: 'control' } });
assert.equal(busy.status, 409);
@@ -291,12 +291,12 @@ test('explicit own-control replacement recovers a viewer without stealing anothe
const refused = await call('POST', route, { headers: { authorization: 'Bearer ' + peer.data.key }, body: { mode: 'control', replace_own_control: true } });
assert.equal(refused.status, 409);
const current = await open({ mode: 'control', replace_own_control: true });
- assert.equal((await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + old } })).status, 401);
- const settings = await (await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + current } })).json();
+ assert.equal((await call('GET', '/desktop/lane/' + old + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + old } })).status, 401);
+ const settings = await (await call('GET', '/desktop/lane/' + current + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + current } })).json();
assert.equal(settings.view_only, false);
const view = await open({ mode: 'view', replace_own_control: true });
- assert.equal((await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + current } })).status, 401);
- assert.equal((await (await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + view } })).json()).view_only, true);
+ assert.equal((await call('GET', '/desktop/lane/' + current + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + current } })).status, 401);
+ assert.equal((await (await call('GET', '/desktop/lane/' + view + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + view } })).json()).view_only, true);
await open({ mode: 'control' });
} finally { await call('POST', '/v1/agents/sessions/' + sessions.a.id + '/handback', { headers: { authorization }, body: {} }); }
});
@@ -322,8 +322,8 @@ test('revoking a credential closes its access and live desktop lease', async ()
const scopedTicket = new URL(scopedView.data.url).searchParams.get('ticket');
const scopedOpened = await call('GET', '/desktop/open?ticket=' + scopedTicket, { raw: true, headers: { authorization: 'Bearer ' + scoped.key } });
const scopedCookie = /nodeviewer=([A-Za-z0-9_-]+)/.exec(scopedOpened.headers.get('set-cookie'))[1];
- assert.equal((await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + scopedCookie } })).status, 200);
- const live = new WebSocket(service.origin.replace('http:', 'ws:') + '/desktop/socket', { headers: { cookie: 'nodeviewer=' + scopedCookie, origin: service.origin } });
+ assert.equal((await call('GET', '/desktop/lane/' + scopedCookie + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + scopedCookie } })).status, 200);
+ const live = new WebSocket(service.origin.replace('http:', 'ws:') + '/desktop/lane/' + scopedCookie + '/socket', { headers: { cookie: 'nodeviewer=' + scopedCookie, origin: service.origin } });
const liveClosed = new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Revoked viewer stayed connected')), 4000);
live.once('close', () => { clearTimeout(timeout); resolve(); });
@@ -333,8 +333,8 @@ test('revoking a credential closes its access and live desktop lease', async ()
assert.equal(revoked.data.revoked, true);
await liveClosed;
assert.equal((await call('GET', '/v1/agents', { headers: { authorization: 'Bearer ' + scoped.key } })).status, 401);
- assert.equal((await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + scopedCookie } })).status, 401);
- assert.equal((await call('GET', '/desktop/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + sessions.viewCookie } })).status, 200, 'an owner viewer stays valid');
+ assert.equal((await call('GET', '/desktop/lane/' + scopedCookie + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + scopedCookie } })).status, 401);
+ assert.equal((await call('GET', '/desktop/lane/' + sessions.viewCookie + '/mandatory.json', { raw: true, headers: { cookie: 'nodeviewer=' + sessions.viewCookie } })).status, 200, 'an owner viewer stays valid');
});
test('credential store and owner key stay private', async () => {
diff --git a/tests/node-agent-codex-adapter.test.mjs b/tests/node-agent-codex-adapter.test.mjs
index 346fe01..4d322e2 100644
--- a/tests/node-agent-codex-adapter.test.mjs
+++ b/tests/node-agent-codex-adapter.test.mjs
@@ -1,12 +1,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import { mkdtemp, readFile, realpath } from 'node:fs/promises';
+import { mkdtemp, readFile, realpath, readlink, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';
+import { PassThrough, Writable } from 'node:stream';
+import { request as httpRequest } from 'node:http';
import { startNodeAgentApi } from '../tools/node-agent-api/server.mjs';
import { createCodexAdapter } from '../tools/node-agent-api/codex.mjs';
-import { rpcParams } from '../tools/node-agent-api/runtime/harness.mjs';
+import { createHarness, rpcParams } from '../tools/node-agent-api/runtime/harness.mjs';
+import { createStore } from '../tools/node-agent-api/store.mjs';
+import { createInputHandler } from '../tools/node-agent-api/input.mjs';
+import fsPromises from 'node:fs/promises';
+import { readFileSync } from 'node:fs';
+import { syncBuiltinESMExports } from 'node:module';
+
+const realTimeout = globalThis.setTimeout, realClearTimeout = globalThis.clearTimeout;
+const nextTick = () => new Promise(resolve => setImmediate(resolve));
+
+async function bounded(promise) {
+ let timer;
+ try { return await Promise.race([promise, new Promise((_, reject) => { timer = realTimeout(() => reject(new Error('Fixture boundary did not complete')), 5000); })]); }
+ finally { realClearTimeout(timer); }
+}
// Fake harness speaking the real RPC seam: requests are validated by rpcParams,
// the same gate the production harness applies. Scripted replies stand in for
@@ -391,3 +407,638 @@ test('handoff unsubscribes the thread and reports it without replay', async () =
await f.service.close();
}
});
+
+async function validatedFixture({ legacy = false, realHarness = false } = {}) {
+ const dir = await realpath(await mkdtemp(join(tmpdir(), 'codex-validated-')));
+ const store = await createStore(join(dir, 'api'));
+ const counts = { ensure: 0, validate: 0, status: 0, containerSave: 0, disconnect: 0 };
+ const identity = { containerId: 'fixture-container', containerStartedAt: 'start-1', assignmentRevision: 'seat-1', display: 5 };
+ const runtime = {
+ ...createFakeRuntime(), shared: true,
+ async ensure() { counts.ensure++; return { running: true, ...identity }; },
+ async status() { counts.status++; return { running: true, ...identity }; },
+ async validateExecutor() { counts.validate++; return { ...identity }; },
+ async restore() { return { restored: true }; },
+ };
+ const wire = [], children = [];
+ const authFile = join(dir, 'dummy-auth.json'), binary = join(dir, 'fake-codex');
+ let harness;
+ if (realHarness) {
+ await writeFile(authFile, '{}', { mode: 0o600 });
+ await writeFile(binary, '#!/bin/sh\nprintf "codex-cli 0.160.0\\n"\n', { mode: 0o700 });
+ runtime.descriptor = async () => ({ environments: [{ ...identity, transport: 'stdio', program: '/fixture/docker', args: ['exec', identity.containerId, '--no-new-privs', 'exec-server', '--listen', 'stdio'] }] });
+ harness = createHarness({ runtime, namespace: 'grok-node-lab-fixture', stateRoot: dir, authFile, binary,
+ spawnProcess(_binary, _args, options) {
+ const child = new EventEmitter();
+ child.stdout = new PassThrough(); child.stderr = new PassThrough(); child.options = options;
+ child.stdin = new Writable({ write(chunk, _encoding, done) {
+ const request = JSON.parse(chunk.toString()); wire.push(request);
+ const result = request.method === 'thread/start' ? { thread: { id: 'thr_native' }, model: 'fixture-model' }
+ : request.method === 'thread/resume' ? { model: 'fixture-model' }
+ : request.method === 'thread/read' ? { thread: { turns: [] } }
+ : request.method === 'turn/start' ? { turn: { id: 'turn_native', status: 'inProgress' } } : {};
+ if (request.id != null) queueMicrotask(() => child.stdout.write(JSON.stringify({ id: request.id, result }) + '\n'));
+ done();
+ } });
+ child.kill = () => { child.killed = true; child.emit('exit', 0); };
+ children.push(child);
+ return child;
+ },
+ });
+ harness.calls = [];
+ } else harness = createFakeHarness();
+ let connection = null, connectionSeq = 0;
+ const originalRequest = harness.request;
+ if (realHarness) {
+ const disconnect = harness.disconnect;
+ harness.disconnect = async botId => { counts.disconnect++; await disconnect(botId); };
+ } else {
+ harness.connectionIdentity = () => connection;
+ harness.disconnect = async () => { counts.disconnect++; connection = null; };
+ }
+ harness.request = async (botId, method, params) => {
+ if (realHarness) harness.calls.push({ botId, method, params });
+ else connection ??= ++connectionSeq;
+ if (method === 'turn/start') {
+ const disk = JSON.parse(await readFile(join(dir, 'api/sessions.json'), 'utf8'));
+ assert.ok(Object.values(disk[0].requests).some(request => request.status === 'pending'), 'pending input must be durable before turn/start');
+ }
+ const result = await originalRequest(botId, method, params);
+ if (!realHarness && (method === 'thread/start' || method === 'thread/resume')) result.model = 'fixture-model';
+ return result;
+ };
+ const update = store.update;
+ store.update = async (id, changes) => {
+ if (Object.hasOwn(changes, 'executor_container_id')) counts.containerSave++;
+ return update.call(store, id, changes);
+ };
+ if (legacy) { delete runtime.validateExecutor; delete harness.connectionIdentity; }
+ const adapter = createCodexAdapter({ runtime, harness, roster, namespace: 'grok-node-lab-fixture', stateRoot: dir });
+ adapter.attachStore(store);
+ const { session } = await store.create('bot-a', {}, { model: 'fixture-model' });
+ const input = createInputHandler({ adapter, store });
+ return {
+ adapter, runtime, harness, store, session, identity, counts, wire, children, authFile,
+ resetCounts() { for (const key of Object.keys(counts)) counts[key] = 0; harness.calls.length = 0; },
+ submit: key => input(session, { events: [{ type: 'message', text: 'synthetic input' }], idempotency_key: key }),
+ async close() { await adapter.close(); await store.flush(); await rm(dir, { recursive: true, force: true }); },
+ };
+}
+
+test('validated warm submit preserves the thread, model and durable dedupe without preparation or container saves', async t => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ const threadId = f.session.thread_id;
+ f.resetCounts();
+ const replies = await Promise.all(Array.from({ length: 4 }, () => f.submit('nreq_validated_warm')));
+ assert.equal(replies.filter(reply => reply.replayed).length, 3);
+ assert.equal(f.session.thread_id, threadId);
+ assert.equal(f.session.turns[0].model, 'fixture-model');
+ assert.equal(f.session.requests.nreq_validated_warm.status, 'accepted');
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['turn/start']);
+ assert.equal(f.harness.calls[0].params.threadId, threadId);
+ assert.equal(f.harness.calls[0].params.model, 'fixture-model');
+ t.diagnostic(JSON.stringify({ scenario: 'warm', ...f.counts, turnStart: f.harness.calls.length }));
+ assert.equal(f.counts.validate, 1);
+ assert.equal(f.counts.ensure, 0);
+ assert.equal(f.counts.status, 0);
+ assert.equal(f.counts.containerSave, 0);
+ } finally { await f.close(); }
+});
+
+for (const [field, value] of [['containerId', 'replacement'], ['containerStartedAt', 'start-2'], ['assignmentRevision', 'seat-2'], ['display', 6]]) {
+ test(`validated submit recovers changed ${field} before sending exactly one new turn`, async t => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ const threadId = f.session.thread_id;
+ f.resetCounts();
+ f.identity[field] = value;
+ const result = await f.submit('nreq_validated_changed');
+ assert.equal(result.status, 'accepted');
+ assert.equal(f.session.thread_id, threadId);
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['thread/resume', 'thread/read', 'turn/start']);
+ assert.equal(f.harness.calls[0].params.model, 'fixture-model');
+ assert.equal(f.harness.calls[2].params.threadId, threadId);
+ assert.equal(f.counts.disconnect, 1);
+ assert.equal(f.counts.ensure, 1);
+ assert.equal(f.session.executor_container_id, f.identity.containerId);
+ assert.equal(f.counts.containerSave, field === 'containerId' ? 1 : 0);
+ t.diagnostic(JSON.stringify({ scenario: field, ...f.counts, turnStart: 1 }));
+ } finally { await f.close(); }
+ });
+}
+
+test('validated cold resume ensures once and restores history before sending the new input', async t => {
+ const f = await validatedFixture();
+ try {
+ await f.store.update(f.session.id, { thread_id: 'thr_existing', executor_container_id: f.identity.containerId, turns: [], actions: [], task_status: 'idle' });
+ f.resetCounts();
+ const request = f.harness.request;
+ f.harness.request = async (...args) => {
+ const result = await request(...args);
+ return args[1] === 'thread/read' ? { thread: { turns: [{ id: 'turn_old', status: 'completed', items: [{ id: 'item_old', type: 'agentMessage', text: 'saved' }] }] } } : result;
+ };
+ await f.submit('nreq_validated_cold');
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['thread/resume', 'thread/read', 'turn/start']);
+ assert.equal(f.session.turns[0].items[0].text, 'saved');
+ assert.equal(f.session.turns[1].request_id, 'nreq_validated_cold');
+ t.diagnostic(JSON.stringify({ scenario: 'cold', ...f.counts, turnStart: 1 }));
+ assert.equal(f.counts.ensure, 1);
+ assert.equal(f.counts.containerSave, 0);
+ } finally { await f.close(); }
+});
+
+test('fresh validation failure refuses a live thread and keeps unknown input non-replayable', async () => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ f.resetCounts();
+ f.runtime.validateExecutor = async () => { throw Object.assign(new Error('Executor removed'), { code: 'execution_unavailable' }); };
+ await assert.rejects(f.submit('nreq_validated_missing'), /Executor removed/);
+ assert.equal(f.session.requests.nreq_validated_missing.status, 'unknown');
+ const replay = await f.submit('nreq_validated_missing');
+ assert.equal(replay.replayed, true);
+ assert.equal(replay.status, 'unknown');
+ assert.equal(f.harness.calls.length, 0);
+ assert.equal(f.runtime.shells.length, 0);
+ assert.equal(f.counts.ensure, 0);
+ } finally { await f.close(); }
+});
+
+for (const failure of ['model mismatch', 'resume timeout']) {
+ test(`changed assignment ${failure} blocks turn/start and does not replay input`, async () => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ f.identity.assignmentRevision = 'new-seat';
+ f.resetCounts();
+ const request = f.harness.request;
+ f.harness.request = async (...args) => {
+ const result = await request(...args);
+ if (args[1] === 'thread/resume') {
+ if (failure === 'resume timeout') throw new Error('RPC timeout; outcome unknown; no replay');
+ return { model: 'wrong-model' };
+ }
+ return result;
+ };
+ await assert.rejects(f.submit('nreq_validated_failed'), failure === 'resume timeout' ? /RPC timeout/ : { code: 'model_mismatch' });
+ assert.equal(f.harness.calls.some(call => call.method === 'turn/start'), false);
+ assert.equal(f.session.requests.nreq_validated_failed.status, 'unknown');
+ assert.equal((await f.submit('nreq_validated_failed')).replayed, true);
+ assert.equal(f.harness.calls.filter(call => call.method === 'thread/resume').length, 1);
+ } finally { await f.close(); }
+ });
+}
+
+for (const invalidation of ['connection lost', 'handoff', 'restore']) {
+ test(`validated preparation is invalid after ${invalidation}`, async () => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ if (invalidation === 'connection lost') await f.harness.disconnect('bot-a');
+ if (invalidation === 'handoff') await f.adapter.handoff(f.session);
+ if (invalidation === 'restore') await f.adapter.restore(f.session, { backup_id: 'fixture-backup' });
+ f.resetCounts();
+ await f.submit('nreq_validated_invalidated');
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['thread/resume', 'thread/read', 'turn/start']);
+ assert.equal(f.counts.ensure, 1);
+ assert.equal(f.session.turns.at(-1).request_id, 'nreq_validated_invalidated');
+ } finally { await f.close(); }
+ });
+}
+
+test('shared desktop forwards preparation policy and fresh assignment identity unchanged', async () => {
+ const runtime = createFakeRuntime();
+ runtime.shared = true;
+ const environment = { containerStartedAt: 'start-1', assignmentRevision: 'seat-1', display: 5 };
+ const options = { prepare: true }, target = { type: 'desktop' };
+ runtime.desktop = async (...args) => { assert.deepEqual(args, ['bot-a', target, 'view', options]); return environment; };
+ const adapter = createCodexAdapter({ runtime, harness: createFakeHarness(), roster });
+ assert.equal(await adapter.desktop('bot-a', target, 'view', options), environment);
+});
+
+test('validated disconnect marks running work unknown and never resubmits its input', async () => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ await f.submit('nreq_validated_disconnect');
+ await f.harness.disconnect('bot-a');
+ f.harness.emit('bot-a', { method: 'lab/status', params: { status: 'unknown', replayed: false } });
+ await f.adapter.flush();
+ assert.equal(f.session.task_status, 'unknown');
+ assert.equal(f.session.turns[0].status, 'unknown');
+ assert.equal(f.session.events.at(-1).data.replayed, false);
+ const calls = f.harness.calls.length;
+ assert.equal((await f.submit('nreq_validated_disconnect')).replayed, true);
+ assert.equal(f.harness.calls.length, calls);
+ } finally { await f.close(); }
+});
+
+test('legacy shared runtime without executor validation retains ensure before live-thread reuse', async () => {
+ const f = await validatedFixture({ legacy: true });
+ try {
+ await f.adapter.bindSession(f.session);
+ f.resetCounts();
+ assert.equal((await f.submit('nreq_legacy_shared')).status, 'accepted');
+ assert.equal(f.counts.ensure, 1);
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['turn/start']);
+ } finally { await f.close(); }
+});
+
+test('cold recovery prepares an explicitly unavailable bot desktop before resuming its thread', async t => {
+ const f = await validatedFixture();
+ try {
+ await f.store.update(f.session.id, { thread_id: 'thr_existing', executor_container_id: f.identity.containerId, turns: [], actions: [], task_status: 'idle' });
+ const validate = f.runtime.validateExecutor;
+ let attempts = 0;
+ f.runtime.validateExecutor = async botId => {
+ attempts += 1;
+ if (attempts === 1) throw Object.assign(new Error('Bot desktop is not running'), { code: 'desktop_unavailable' });
+ return validate(botId);
+ };
+ f.resetCounts();
+ assert.equal((await f.submit('nreq_cold_desktop_unavailable')).status, 'accepted');
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['thread/resume', 'thread/read', 'turn/start']);
+ assert.equal(f.session.turns.at(-1).request_id, 'nreq_cold_desktop_unavailable');
+ assert.equal(f.session.executor_container_id, f.identity.containerId);
+ t.diagnostic(JSON.stringify({ scenario: 'cold-desktop-unavailable', attempts, ...f.counts }));
+ assert.equal(attempts, 2);
+ assert.equal(f.counts.ensure, 1);
+ } finally { await f.close(); }
+});
+
+for (const [label, code] of [['stale executor token', 401], ['ownership denial', 403], ['transport fault', 502]]) {
+ test(`cold recovery fails closed on ${label} without preparing`, async () => {
+ const f = await validatedFixture();
+ try {
+ await f.store.update(f.session.id, { thread_id: 'thr_existing', executor_container_id: f.identity.containerId, turns: [], actions: [], task_status: 'idle' });
+ f.runtime.validateExecutor = async () => { throw Object.assign(new Error('Executor identity refused'), { code: code === 403 ? 'permission_denied' : 'execution_failed', status: code }); };
+ f.resetCounts();
+ await assert.rejects(f.submit('nreq_cold_refused'), /Executor identity refused/);
+ assert.equal(f.session.requests.nreq_cold_refused.status, 'unknown');
+ assert.equal(f.harness.calls.length, 0);
+ assert.equal(f.runtime.shells.length, 0);
+ assert.equal(f.counts.ensure, 0);
+ assert.equal((await f.submit('nreq_cold_refused')).replayed, true);
+ assert.equal(f.harness.calls.length, 0);
+ } finally { await f.close(); }
+ });
+}
+
+test('a prepared generation going unavailable fails closed instead of preparing', async () => {
+ const f = await validatedFixture();
+ try {
+ await f.adapter.bindSession(f.session);
+ f.runtime.validateExecutor = async () => { throw Object.assign(new Error('Bot desktop is not running'), { code: 'desktop_unavailable' }); };
+ f.resetCounts();
+ await assert.rejects(f.submit('nreq_prepared_desktop_unavailable'), /Bot desktop is not running/);
+ assert.equal(f.counts.ensure, 0);
+ assert.equal(f.harness.calls.length, 0);
+ assert.equal(f.runtime.shells.length, 0);
+ assert.equal(f.session.requests.nreq_prepared_desktop_unavailable.status, 'unknown');
+ } finally { await f.close(); }
+});
+
+test('real Harness uses explicit fixture auth, prepares once, reuses warm connection and recovers replacement', async t => {
+ const f = await validatedFixture({ realHarness: true });
+ try {
+ await f.adapter.bindSession(f.session);
+ assert.equal(f.counts.ensure, 1);
+ assert.equal(f.children.length, 1);
+ const home = f.children[0].options.cwd;
+ assert.equal(await readlink(join(home, 'auth.json')), f.authFile);
+ assert.match(await readFile(join(home, 'environments.toml'), 'utf8'), /include_local = false/);
+ const initialConnection = f.harness.connectionIdentity('bot-a');
+ f.resetCounts();
+ assert.equal((await f.submit('nreq_real_warm')).status, 'accepted');
+ assert.equal(f.counts.ensure, 0);
+ assert.equal(f.counts.status, 0);
+ assert.equal(f.counts.validate, 1);
+ assert.equal(f.counts.containerSave, 0);
+ assert.equal(f.children.length, 1);
+ assert.equal(f.harness.connectionIdentity('bot-a'), initialConnection);
+ assert.equal(f.wire.filter(call => call.method === 'turn/start').length, 1);
+ f.children[0].stdout.write(JSON.stringify({ method: 'turn/completed', params: { threadId: f.session.thread_id, turn: { id: 'turn_native', status: 'completed' } } }) + '\n');
+ await f.adapter.flush();
+ f.identity.containerId = 'native-replacement';
+ f.resetCounts();
+ assert.equal((await f.submit('nreq_real_replacement')).status, 'accepted');
+ assert.equal(f.counts.ensure, 1);
+ assert.equal(f.counts.disconnect, 1);
+ assert.equal(f.children.length, 2);
+ assert.equal(f.children[0].killed, true);
+ assert.notEqual(f.harness.connectionIdentity('bot-a'), initialConnection);
+ assert.match(await readFile(join(home, 'environments.toml'), 'utf8'), /native-replacement/);
+ assert.deepEqual(f.harness.calls.map(call => call.method), ['thread/resume', 'thread/read', 'turn/start']);
+ assert.equal(f.wire.filter(call => call.method === 'turn/start').length, 2);
+ t.diagnostic(JSON.stringify({ scenario: 'real-harness-replacement', ...f.counts, spawned: f.children.length, turnStart: 2 }));
+ } finally { await f.close(); }
+ assert.ok(f.children.every(child => child.killed));
+});
+
+async function batchFixture(t, { http = false, botCount = 1 } = {}) {
+ const dir = await realpath(await mkdtemp(join(tmpdir(), 'codex-batch-')));
+ const file = join(dir, 'api/sessions.json');
+ const harness = createFakeHarness(), runtime = createFakeRuntime();
+ const adapter = createCodexAdapter({ runtime, harness, roster, namespace: 'grok-node-lab-fixture', stateRoot: dir });
+ let store, service, key;
+ if (http) {
+ const attachStore = adapter.attachStore;
+ adapter.attachStore = value => { store = value; attachStore(value); };
+ service = await startNodeAgentApi({ adapter, stateDirectory: join(dir, 'api') });
+ key = (await readFile(service.keyFile, 'utf8')).trim();
+ } else {
+ store = await createStore(join(dir, 'api'));
+ adapter.attachStore(store);
+ }
+ const sessions = [];
+ for (const bot of bots.slice(0, botCount)) {
+ const { session } = await store.create(bot.id, {}, { reuse: false });
+ await adapter.bindSession(session);
+ await store.update(session.id, { turns: [{ object: 'node.turn', id: 'turn_' + bot.id, status: 'running', items: [{ id: 'item_' + bot.id, type: 'agentMessage', text: '' }], usage: null }], active_turn_id: 'turn_' + bot.id, task_status: 'running' });
+ sessions.push(session);
+ }
+ const commits = [], published = [];
+ let renameFault;
+ const rename = fsPromises.rename;
+ t.mock.method(fsPromises, 'rename', async (from, to) => {
+ if (to === file && renameFault) throw renameFault;
+ const result = await rename(from, to);
+ if (to === file) commits.push(JSON.parse(readFileSync(file, 'utf8')));
+ return result;
+ });
+ syncBuiltinESMExports();
+ t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
+ const unsubscribe = store.subscribeAll(event => {
+ const disk = JSON.parse(readFileSync(file, 'utf8')).find(session => session.id === event.session_id);
+ assert.deepEqual(disk.events.find(row => row.id === event.id), event, 'the full envelope must be durable before publication');
+ published.push(event);
+ });
+ const delta = (index, session = sessions[0], method = 'item/agentMessage/delta') => ({ method, params: { threadId: session.thread_id, turnId: session.active_turn_id, itemId: 'item_' + session.agent_id, delta: '片段-' + index } });
+ const emit = (message, session = sessions[0]) => harness.emit(session.agent_id, message);
+ const notifications = session => session.events.filter(event => event.type === 'node.harness.event');
+ const controllers = new Set();
+ // Keep real HTTP transport independent of the mocked batching timer.
+ async function responseFor(route, after, stream = false) {
+ const controller = new AbortController(); controllers.add(controller);
+ try {
+ const response = await new Promise((resolve, reject) => {
+ const request = httpRequest(service.origin + route, {
+ agent: false, signal: controller.signal,
+ headers: { authorization: 'Bearer ' + key, ...(stream ? { accept: 'text/event-stream' } : {}), ...(after ? { 'Last-Event-ID': after } : {}) },
+ }, resolve);
+ request.once('error', reject); request.end();
+ });
+ response.once('close', () => controllers.delete(controller));
+ assert.equal(response.statusCode, 200);
+ return { response, close() { controller.abort(); controllers.delete(controller); } };
+ } catch (error) { controller.abort(); controllers.delete(controller); throw error; }
+ }
+ return {
+ dir, file, adapter, harness, runtime, store, service, sessions, commits, published, delta, emit, notifications,
+ failRename(error) { renameFault = error; },
+ disk: () => JSON.parse(readFileSync(file, 'utf8')),
+ async call(route) {
+ const { response } = await responseFor(route);
+ const chunks = [];
+ for await (const chunk of response) chunks.push(chunk);
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
+ },
+ async stream(route, after) {
+ const { response, close } = await responseFor(route, after, true);
+ const reader = response[Symbol.asyncIterator](), decoder = new TextDecoder();
+ let text = '';
+ return {
+ async until(count) {
+ while ((text.match(/\ndata: /g) ?? []).length < count) {
+ const part = await bounded(reader.next());
+ if (part.done) throw new Error('SSE ended before its committed events');
+ text += decoder.decode(part.value, { stream: true });
+ }
+ return text.split('\n\n').filter(frame => frame.includes('\ndata: ')).map(frame => JSON.parse(frame.split('\ndata: ')[1]));
+ },
+ close,
+ };
+ },
+ async close({ fault = false } = {}) {
+ for (const controller of controllers) controller.abort();
+ unsubscribe();
+ try {
+ await (service ? service.close() : adapter.close());
+ await store.flush();
+ } catch (error) { if (!fault) throw error; }
+ finally { await rm(dir, { recursive: true, force: true }); }
+ },
+ };
+}
+
+test('batch: a low-frequency ordinary delta commits at 25ms, never at 24ms', async t => {
+ const f = await batchFixture(t);
+ try {
+ let attempts = 0; f.store.setWriteGuard(async () => { attempts++; });
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const message = f.delta(0); f.emit(message);
+ await nextTick();
+ assert.equal(attempts, 0);
+ t.mock.timers.tick(24); await nextTick();
+ assert.equal(attempts, 0);
+ assert.equal(f.notifications(f.sessions[0]).length, 0);
+ t.mock.timers.tick(1); await nextTick(); await f.store.flush();
+ assert.equal(attempts, 1);
+ assert.equal(f.commits.length, 1);
+ assert.deepEqual(f.notifications(f.sessions[0]).map(event => event.data), [message]);
+ } finally { t.mock.timers.reset(); await f.close(); }
+});
+
+test('batch: the 64th delta flushes immediately and the 65th stays buffered until its own deadline', async t => {
+ const f = await batchFixture(t);
+ try {
+ let attempts = 0; f.store.setWriteGuard(async () => { attempts++; });
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const expected = Array.from({ length: 65 }, (_, index) => f.delta(index));
+ for (const message of expected.slice(0, 63)) f.emit(message);
+ await nextTick(); assert.equal(attempts, 0);
+ f.emit(expected[63]); await nextTick(); await f.store.flush();
+ assert.equal(f.commits.length, 1);
+ assert.deepEqual(f.notifications(f.sessions[0]).map(event => event.data), expected.slice(0, 64));
+ f.emit(expected[64]); await nextTick(); assert.equal(attempts, 1);
+ t.mock.timers.tick(25); await nextTick(); await f.store.flush();
+ assert.equal(f.commits.length, 2);
+ assert.deepEqual(f.notifications(f.sessions[0]).map(event => event.data), expected);
+ } finally { t.mock.timers.reset(); await f.close(); }
+});
+
+test('batch: a 256-delta cross-bot burst reduces actual disk commits by at least 80% without losing envelopes or cursor order', async t => {
+ const f = await batchFixture(t, { botCount: 2 });
+ try {
+ const expected = [];
+ for (let index = 0; index < 256; index++) {
+ const session = f.sessions[index % 2], message = f.delta(index, session);
+ expected.push({ session_id: session.id, ...message }); f.emit(message, session);
+ }
+ await f.adapter.flush();
+ assert.equal(f.published.length, expected.length);
+ assert.deepEqual(f.published.map(event => ({ session_id: event.session_id, ...event.data })), expected);
+ for (const session of f.sessions) {
+ const events = f.notifications(session);
+ assert.deepEqual(events.map(event => event.data), expected.filter(row => row.session_id === session.id).map(({ session_id, ...message }) => message));
+ assert.equal(new Set(events.map(event => event.id)).size, events.length);
+ assert.equal(f.disk().find(row => row.id === session.id).events.length, session.events.length);
+ }
+ const reduction = 1 - f.commits.length / expected.length;
+ t.diagnostic(JSON.stringify({ scenario: 'delta-burst', deltas: expected.length, diskCommits: f.commits.length, reduction, published: f.published.length }));
+ assert.ok(reduction >= 0.8, 'a burst must remove at least 80% of actual atomic disk writes');
+ } finally { await f.close(); }
+});
+
+for (const boundary of ['terminal', 'approval', 'item completed', 'disconnect']) {
+ test(`batch: ${boundary} flushes preceding deltas without waiting for 25ms and commits its state with its event`, async t => {
+ const f = await batchFixture(t);
+ try {
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const session = f.sessions[0], first = f.delta(0), second = f.delta(1);
+ f.emit(first); f.emit(second);
+ const critical = boundary === 'terminal' ? { method: 'turn/completed', params: { threadId: session.thread_id, turn: { id: session.active_turn_id, status: 'completed' } } }
+ : boundary === 'approval' ? { id: 'rpc_batch_approval', method: 'item/tool/call', params: { threadId: session.thread_id, turnId: session.active_turn_id, itemId: 'tool_item', tool: 'node_approved_shell', arguments: { command: 'synthetic-only' } } }
+ : boundary === 'item completed' ? { method: 'item/completed', params: { threadId: session.thread_id, turnId: session.active_turn_id, item: { id: 'item_bot-a', type: 'agentMessage', text: '片段-0片段-1' } } }
+ : { method: 'lab/status', params: { status: 'unknown', replayed: false } };
+ let observed;
+ const delivery = new Promise(resolve => { const off = f.store.subscribe(session.id, event => {
+ if (event.data?.method === critical.method || (boundary === 'disconnect' && event.type === 'node.session.turn.unknown')) {
+ observed = f.disk()[0]; off(); resolve(event);
+ }
+ }); });
+ f.emit(critical); await bounded(delivery);
+ assert.deepEqual(f.published.slice(0, 2).map(event => event.data), [first, second]);
+ if (boundary === 'terminal') { assert.equal(observed.task_status, 'completed'); assert.equal(observed.active_turn_id, null); }
+ if (boundary === 'approval') assert.equal(observed.actions[0].status, 'pending');
+ if (boundary === 'item completed') assert.equal(observed.turns[0].items[0].text, '片段-0片段-1');
+ if (boundary === 'disconnect') { assert.equal(observed.turns[0].status, 'unknown'); assert.equal(f.published.at(-1).data.replayed, false); }
+ t.diagnostic(JSON.stringify({ scenario: boundary, diskCommits: f.commits.length, published: f.published.length }));
+ assert.equal(f.commits.length, 2, 'one delta batch and one atomic critical notification');
+ } finally { t.mock.timers.reset(); await f.close(); }
+ });
+}
+
+for (const boundary of ['items', 'actions', 'cancel', 'flush', 'close']) {
+ test(`batch: adapter ${boundary} drains already-received deltas before returning or issuing a side effect`, async t => {
+ const f = await batchFixture(t);
+ try {
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const session = f.sessions[0], message = f.delta(0);
+ const request = f.harness.request;
+ f.harness.request = async (...args) => {
+ if (args[1] === 'turn/interrupt') assert.deepEqual(f.notifications(session).map(event => event.data), [message]);
+ return request(...args);
+ };
+ const close = f.harness.close;
+ f.harness.close = async () => { assert.deepEqual(f.disk()[0].events.filter(event => event.type === 'node.harness.event').map(event => event.data), [message]); await close(); };
+ f.emit(message);
+ if (boundary === 'items') await bounded(f.adapter.itemsSession(session));
+ if (boundary === 'actions') await bounded(f.adapter.actions(session));
+ if (boundary === 'cancel') await bounded(f.adapter.cancel(session, session.active_turn_id));
+ if (boundary === 'flush') await bounded(f.adapter.flush());
+ if (boundary === 'close') await bounded(f.adapter.close());
+ assert.deepEqual(f.notifications(session).map(event => event.data), [message]);
+ assert.equal(f.published.length, 1);
+ } finally { t.mock.timers.reset(); await f.close(); }
+ });
+}
+
+test('batch: input drains preceding deltas before rejecting an already-running turn and never replays it', async t => {
+ const f = await batchFixture(t);
+ try {
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const session = f.sessions[0], message = f.delta(0);
+ f.emit(message);
+ await assert.rejects(bounded(f.adapter.inputSession(session, 'synthetic next input', 'nreq_batch_input')), { code: 'turn_active' });
+ assert.deepEqual(f.notifications(session).map(event => event.data), [message]);
+ assert.equal(f.harness.calls.filter(call => call.method === 'turn/start').length, 0);
+ } finally { t.mock.timers.reset(); await f.close(); }
+});
+
+test('batch: a rename fault publishes no pending delta and poisons subsequent adapter operations without replay', async t => {
+ const f = await batchFixture(t);
+ try {
+ const before = f.store.snapshot(), onDisk = readFileSync(f.file, 'utf8');
+ f.failRename(new Error('Synthetic atomic rename fault'));
+ f.emit(f.delta(0)); f.emit(f.delta(1));
+ await assert.rejects(f.adapter.flush(), /Synthetic atomic rename fault/);
+ assert.equal(f.store.snapshot(), before);
+ assert.equal(readFileSync(f.file, 'utf8'), onDisk);
+ assert.equal(f.published.length, 0);
+ assert.equal(f.commits.length, 0);
+ f.failRename(null);
+ await assert.rejects(f.adapter.itemsSession(f.sessions[0]), /Synthetic atomic rename fault/);
+ assert.equal(f.harness.calls.some(call => call.method === 'turn/start'), false);
+ } finally { f.failRename(null); await f.close({ fault: true }); }
+});
+
+test('batch: answering an approval drains prior deltas and keeps the responding marker durable before exactly one tool execution', async t => {
+ const f = await batchFixture(t);
+ try {
+ const session = f.sessions[0];
+ f.emit({ id: 'rpc_batch_tool', method: 'item/tool/call', params: { threadId: session.thread_id, turnId: session.active_turn_id, itemId: 'tool_batch', tool: 'node_approved_shell', arguments: { command: 'synthetic-only' } } });
+ await f.adapter.flush();
+ const action = session.actions[0];
+ f.commits.length = 0; f.published.length = 0;
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ const message = f.delta(0); f.emit(message);
+ const shell = f.runtime.approvedShell;
+ f.runtime.approvedShell = async (...args) => {
+ const disk = f.disk()[0];
+ assert.equal(disk.actions[0].status, 'responding');
+ assert.deepEqual(disk.events.filter(event => event.data?.method === message.method).map(event => event.data), [message]);
+ return shell(...args);
+ };
+ const answers = await Promise.allSettled([f.adapter.answer(session, action.id, 'accept'), f.adapter.answer(session, action.id, 'accept')]);
+ assert.equal(answers.filter(row => row.status === 'fulfilled').length, 1);
+ assert.equal(f.runtime.shells.length, 1);
+ assert.equal(f.harness.answers.length, 1);
+ assert.equal(session.actions[0].status, 'accept');
+ const responding = f.commits.find(snapshot => snapshot[0].actions[0].status === 'responding');
+ assert.ok(responding);
+ assert.equal(responding[0].events.some(event => event.type === 'node.action.resolved'), false);
+ assert.equal(f.published.at(-1).type, 'node.action.resolved');
+ } finally { t.mock.timers.reset(); await f.close(); }
+});
+
+test('batch: ordinary reasoning and command-output deltas retain their native envelopes in one commit', async t => {
+ const f = await batchFixture(t);
+ try {
+ const methods = ['item/agentMessage/delta', 'item/reasoning/textDelta', 'item/commandExecution/outputDelta'];
+ const messages = methods.map((method, index) => f.delta(index, f.sessions[0], method));
+ for (const message of messages) f.emit(message);
+ await f.adapter.flush();
+ assert.deepEqual(f.notifications(f.sessions[0]).map(event => event.data), messages);
+ assert.equal(f.commits.length, 1);
+ } finally { await f.close(); }
+});
+
+test('batch: JSON history and initial SSE show committed events only, then live flush and cursor reconnect deliver every delta once', async t => {
+ const f = await batchFixture(t, { http: true });
+ try {
+ const session = f.sessions[0], route = '/v1/agents/sessions/' + session.id + '/events';
+ const committed = session.events.slice(), first = f.delta(0), second = f.delta(1);
+ t.mock.timers.enable({ apis: ['setTimeout'] });
+ f.emit(first); f.emit(second);
+ const history = await bounded(f.call(route));
+ assert.deepEqual(history.data, committed);
+ assert.equal(f.published.length, 0);
+ const stream = await bounded(f.stream(route));
+ assert.deepEqual(await stream.until(committed.length), committed);
+ assert.equal(f.commits.length, 0);
+ await f.adapter.flush();
+ const all = await stream.until(committed.length + 2);
+ assert.deepEqual(all.slice(committed.length).map(event => event.data), [first, second]);
+ assert.equal(new Set(all.map(event => event.id)).size, all.length);
+ stream.close();
+ const cursor = all.at(-2).id;
+ const resumed = await bounded(f.stream(route, cursor));
+ assert.deepEqual(await resumed.until(1), [all.at(-1)]);
+ resumed.close();
+ assert.deepEqual((await f.call(route)).data, all);
+ } finally { t.mock.timers.reset(); await f.close(); }
+});
diff --git a/tests/node-agent-desktop-performance.test.mjs b/tests/node-agent-desktop-performance.test.mjs
new file mode 100644
index 0000000..c1ce287
--- /dev/null
+++ b/tests/node-agent-desktop-performance.test.mjs
@@ -0,0 +1,284 @@
+import assert from 'node:assert/strict';
+import { once } from 'node:events';
+import { createServer } from 'node:http';
+import { mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import test from 'node:test';
+import { WebSocket, WebSocketServer } from 'ws';
+import { startNodeAgentApi } from '../tools/node-agent-api/server.mjs';
+import { ApiError } from '../tools/node-agent-api/errors.mjs';
+
+const evidence = { admissions: [], assets: [], renewals: [], cleanup: [] };
+const record = (group, scenario, result) => evidence[group].push({ scenario, ...result });
+
+async function fixture(t) {
+ const stateDirectory = await realpath(await mkdtemp(path.join(tmpdir(), 'desktop-admission-')));
+ const upstreamHttp = createServer();
+ const upstream = new WebSocketServer({ server: upstreamHttp });
+ const state = { running: true, assigned: true, application: true, revision: 'opaque-a', startedAt: 'start-a', generation: 'container-a', bytes: 'export const revision = 1;', html: '
fixture', css: 'body { color: black; }', legacy: false };
+ const counts = { prepare: 0, upstream: 0, assets: 0, options: [] };
+ upstream.on('connection', socket => { counts.upstream++; socket.send(Buffer.from('RFB 003.008\n')); });
+ await new Promise((resolve, reject) => { upstreamHttp.once('error', reject); upstreamHttp.listen(0, '127.0.0.1', resolve); });
+ const bot = { object: 'node.agent', id: 'bot-a', name: 'Fixture', backend: 'grok_node' };
+ const adapter = {
+ writesEnabled: true,
+ async agents() { return [bot]; },
+ async requireAgent() { return bot; },
+ async items() { return []; },
+ async desktop(id, target = { type: 'desktop' }, mode = 'view', options = { prepare: true }) {
+ counts.options.push(options.prepare);
+ if (!state.running) throw new ApiError(409, 'container_stopped', 'Fixture stopped');
+ if (!state.assigned) {
+ if (!options.prepare) throw new ApiError(409, 'desktop_unavailable', 'Fixture unassigned');
+ counts.prepare++; state.assigned = true;
+ }
+ if (target.type === 'application' && !state.application) throw new ApiError(404, 'application_not_found', 'Fixture application closed');
+ return { display: 3, websocketUrl: 'ws://127.0.0.1:' + upstreamHttp.address().port,
+ containerGenerationImmutableId: state.generation,
+ applicationId: target.application_id ?? null,
+ ...(!state.legacy ? { assignmentRevision: state.revision, containerStartedAt: state.startedAt } : {}) };
+ },
+ async viewerAsset(asset) {
+ counts.assets++;
+ state.assetEntered?.();
+ if (state.assetBarrier) await state.assetBarrier;
+ if (!state.running) throw new ApiError(409, 'container_stopped', 'Fixture stopped');
+ if (asset === 'core/rfb.js') return Buffer.from(state.bytes);
+ if (asset === 'vnc.html') return Buffer.from(state.html);
+ if (asset === 'app/style.css') return Buffer.from(state.css);
+ throw new ApiError(404, 'not_found', 'Fixture asset missing');
+ },
+ };
+ const service = await startNodeAgentApi({ adapter, stateDirectory });
+ const authorization = 'Bearer ' + (await readFile(service.keyFile, 'utf8')).trim();
+ async function request(route, { method = 'GET', body, headers = {} } = {}) {
+ return fetch(service.origin + route, { method, headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...headers },
+ ...(body ? { body: JSON.stringify(body) } : {}), redirect: 'manual', signal: AbortSignal.timeout(5000) });
+ }
+ const created = await request('/v1/agents/sessions', { method: 'POST', body: { agent_id: 'bot-a' }, headers: { authorization } });
+ assert.equal(created.status, 201);
+ const session = await created.json();
+ async function issue(body = {}, auth = authorization) {
+ const response = await request('/v1/agents/sessions/' + session.id + '/desktop', { method: 'POST', body, headers: { authorization: auth } });
+ assert.equal(response.status, 201);
+ const url = new URL((await response.json()).url);
+ return url.pathname + url.search;
+ }
+ async function open(ticket) {
+ const response = await request(ticket);
+ const cookie = response.headers.get('set-cookie')?.split(';')[0];
+ return { response, cookie, base: response.headers.get('location')?.replace(/vnc\.html$/, '') };
+ }
+ async function upgrade(viewer, expected = 101, origin = service.origin) {
+ const socket = new WebSocket(service.origin.replace('http:', 'ws:') + viewer.base + 'socket', { headers: { cookie: viewer.cookie, origin }, handshakeTimeout: 4000 });
+ const closed = new Promise(resolve => socket.once('close', resolve));
+ socket.on('error', () => {});
+ try {
+ const status = await new Promise((resolve, reject) => {
+ socket.once('open', () => resolve(101));
+ socket.once('unexpected-response', (_req, response) => { response.resume(); resolve(response.statusCode); });
+ socket.once('error', () => reject(new Error('Fixture upgrade failed without HTTP response')));
+ });
+ if (status === 101 && expected === 101) {
+ const [bytes] = await once(socket, 'message');
+ assert.equal(bytes.toString(), 'RFB 003.008\n');
+ }
+ assert.equal(status, expected);
+ return status;
+ } finally { socket.terminate(); await closed; }
+ }
+ t.after(async () => {
+ await service.close();
+ for (const socket of upstream.clients) socket.terminate();
+ await new Promise(resolve => upstream.close(resolve));
+ await new Promise(resolve => upstreamHttp.close(resolve));
+ await rm(stateDirectory, { recursive: true, force: true });
+ record('cleanup', t.name, { apiClosed: true, upstreamClosed: true, temporaryStateRemoved: true });
+ });
+ return { state, counts, request, issue, open, upgrade, authorization };
+}
+
+test('issue may prepare once; open and upgrade validate and deliver upstream RFB', async t => {
+ const f = await fixture(t);
+ f.state.assigned = false;
+ const ticket = await f.issue();
+ const viewer = await f.open(ticket);
+ assert.equal(viewer.response.status, 303);
+ assert.equal(viewer.response.headers.get('cache-control'), 'no-store');
+ await f.upgrade(viewer);
+ assert.deepEqual(f.counts.options, [true, false, false]);
+ assert.equal(f.counts.prepare, 1);
+ assert.equal(f.counts.upstream, 1);
+ assert.equal((await f.request(ticket)).status, 401);
+ record('admissions', t.name, { options: f.counts.options, preparations: f.counts.prepare, upstreamConnections: f.counts.upstream, replayStatus: 401, rfbBannerReceived: true });
+});
+
+const changes = {
+ assignmentRevision: state => { state.revision = 'opaque-b'; },
+ containerStartedAt: state => { state.startedAt = 'start-b'; },
+ containerId: state => { state.generation = 'container-b'; },
+ missingRevision: state => { state.legacy = true; },
+ stopped: state => { state.running = false; },
+ unassigned: state => { state.assigned = false; },
+ applicationClosed: state => { state.application = false; },
+};
+for (const stage of ['open', 'upgrade']) for (const [change, mutate] of Object.entries(changes)) {
+ test(stage + ' rejects ' + change + ' without preparing or connecting upstream', async t => {
+ const f = await fixture(t);
+ const body = change === 'applicationClosed' ? { target: { type: 'application', application_id: '0x200004' } } : {};
+ const ticket = await f.issue(body);
+ const viewer = stage === 'upgrade' ? await f.open(ticket) : null;
+ mutate(f.state);
+ const expected = change === 'applicationClosed' ? 404 : 409;
+ const status = stage === 'open' ? (await f.request(ticket)).status : await f.upgrade(viewer, expected);
+ assert.equal(status, expected);
+ assert.equal(f.counts.prepare, 0);
+ assert.equal(f.counts.upstream, 0);
+ record('admissions', t.name, { status, preparations: f.counts.prepare, upstreamConnections: f.counts.upstream });
+ });
+}
+
+test('legacy environments without revision fields still open and upgrade', async t => {
+ const f = await fixture(t); f.state.legacy = true;
+ const viewer = await f.open(await f.issue());
+ assert.equal(viewer.response.status, 303);
+ await f.upgrade(viewer);
+ record('admissions', t.name, { openStatus: 303, upgradeStatus: 101, rfbBannerReceived: true });
+});
+
+test('static assets revalidate private ETags against current bytes; dynamic settings stay no-store', async t => {
+ const f = await fixture(t);
+ const viewer = await f.open(await f.issue());
+ const headers = { cookie: viewer.cookie };
+ const first = await f.request(viewer.base + 'core/rfb.js', { headers });
+ assert.equal(first.status, 200);
+ assert.equal(await first.text(), f.state.bytes);
+ const etag = first.headers.get('etag');
+ assert.ok(etag);
+ assert.equal(first.headers.get('cache-control'), 'private, no-cache');
+ assert.equal(first.headers.get('vary'), null);
+ const conditional = { ...headers, 'if-none-match': etag };
+ const cached = await f.request(viewer.base + 'core/rfb.js', { headers: conditional });
+ assert.equal(cached.status, 304); assert.equal(await cached.text(), '');
+ assert.equal(cached.headers.get('cache-control'), 'private, no-cache');
+ assert.equal(f.counts.assets, 2, 'conditional requests must still validate runtime asset generation');
+ for (const condition of ['W/' + etag, '"unrelated", ' + etag, '*']) {
+ const response = await f.request(viewer.base + 'core/rfb.js', { headers: { ...headers, 'if-none-match': condition } });
+ assert.equal(response.status, 304); assert.equal(await response.text(), '');
+ }
+ f.state.bytes = 'export const revision = 2;';
+ const changed = await f.request(viewer.base + 'core/rfb.js', { headers: conditional });
+ assert.equal(changed.status, 200); assert.equal(await changed.text(), f.state.bytes);
+ assert.notEqual(changed.headers.get('etag'), etag);
+ for (const asset of ['mandatory.json', 'defaults.json']) {
+ const response = await f.request(viewer.base + asset, { headers: { ...headers, 'if-none-match': '*' } });
+ assert.equal(response.status, 200); assert.equal(response.headers.get('cache-control'), 'no-store');
+ assert.equal(response.headers.get('etag'), null); await response.json();
+ }
+ const html = await f.request(viewer.base + 'vnc.html', { headers });
+ assert.match(await html.text(), /#noVNC_control_bar/);
+ assert.ok(html.headers.get('etag'));
+ const missing = await f.request(viewer.base + 'core/missing.js', { headers: conditional });
+ assert.equal(missing.status, 404); assert.equal(missing.headers.get('cache-control'), 'no-store');
+ assert.equal(missing.headers.get('etag'), null);
+ f.state.running = false;
+ const stopped = await f.request(viewer.base + 'core/rfb.js', { headers: conditional });
+ assert.equal(stopped.status, 409); assert.equal(stopped.headers.get('cache-control'), 'no-store');
+ record('assets', t.name, { firstStatus: 200, cachedStatus: 304, changedStatus: 200, stoppedStatus: 409, privateRevalidation: true, dynamicNoStore: true });
+});
+
+for (const [asset, field, changedBytes] of [
+ ['vnc.html', 'html', 'updated fixture'],
+ ['core/rfb.js', 'bytes', 'export const revision = 2;'],
+ ['app/style.css', 'css', 'body { color: blue; }'],
+]) {
+ test('static ' + asset + ' reuses content ETag across viewer reauthorization without Cookie Vary', async t => {
+ const f = await fixture(t);
+ const firstViewer = await f.open(await f.issue({ mode: 'view' }));
+ assert.equal(firstViewer.response.status, 303);
+ const first = await f.request(firstViewer.base + asset, { headers: { cookie: firstViewer.cookie } });
+ assert.equal(first.status, 200);
+ const body = await first.text(), etag = first.headers.get('etag');
+ assert.ok(etag);
+ const secondViewer = await f.open(await f.issue({ mode: 'view' }));
+ assert.equal(secondViewer.response.status, 303);
+ assert.notEqual(secondViewer.cookie, firstViewer.cookie);
+ assert.notEqual(secondViewer.base, firstViewer.base);
+ const headers = { cookie: secondViewer.cookie, 'if-none-match': etag };
+ const reused = await f.request(secondViewer.base + asset, { headers });
+ assert.equal(reused.status, 304); assert.equal(await reused.text(), '');
+ assert.equal(reused.headers.get('etag'), etag);
+ assert.equal(f.counts.assets, 2, 'new viewer conditional requests must load current asset bytes');
+ f.state[field] = changedBytes;
+ const changed = await f.request(secondViewer.base + asset, { headers });
+ assert.equal(changed.status, 200); assert.notEqual(changed.headers.get('etag'), etag);
+ const expectedBody = asset === 'vnc.html' ? body.replace('fixture', 'updated fixture') : changedBytes;
+ assert.equal(await changed.text(), expectedBody);
+ for (const response of [first, reused, changed]) {
+ assert.equal(response.headers.get('cache-control'), 'private, no-cache');
+ assert.equal(response.headers.get('vary'), null, 'viewer-independent static bytes must not partition the cache by cookie');
+ }
+ record('renewals', t.name, { asset, cookiesDiffer: true, firstStatus: first.status, renewedStatus: reused.status, changedStatus: changed.status, vary: reused.headers.get('vary'), cacheControl: reused.headers.get('cache-control'), assetReads: f.counts.assets, sameEtagOnRenewal: true, changedEtagOnNewBytes: true });
+ });
+}
+
+test('conditional assets require an active viewer and reject revoked keys before 304', async t => {
+ const f = await fixture(t);
+ const keyResponse = await f.request('/v1/keys', { method: 'POST', headers: { authorization: f.authorization }, body: { scopes: ['sessions.read', 'desktop.view'], bot_ids: ['bot-a'] } });
+ assert.equal(keyResponse.status, 201);
+ const key = await keyResponse.json();
+ const viewer = await f.open(await f.issue({}, 'Bearer ' + key.key));
+ const asset = viewer.base + 'core/rfb.js';
+ const first = await f.request(asset, { headers: { cookie: viewer.cookie } });
+ const etag = first.headers.get('etag'); await first.arrayBuffer();
+ assert.ok(etag);
+ assert.equal((await f.request(asset, { headers: { 'if-none-match': etag } })).status, 401);
+ assert.equal((await f.request('/v1/keys/' + key.id, { method: 'DELETE', headers: { authorization: f.authorization } })).status, 200);
+ const revoked = await f.request(asset, { headers: { cookie: viewer.cookie, 'if-none-match': etag } });
+ assert.equal(revoked.status, 401); assert.equal(revoked.headers.get('cache-control'), 'no-store');
+ assert.equal(f.counts.assets, 1);
+ record('assets', t.name, { missingViewerStatus: 401, revokedViewerStatus: 401, assetReads: f.counts.assets });
+});
+
+test('revocation while an asset loads prevents a conditional response', async t => {
+ const f = await fixture(t);
+ const keyResponse = await f.request('/v1/keys', { method: 'POST', headers: { authorization: f.authorization }, body: { scopes: ['sessions.read', 'desktop.view'], bot_ids: ['bot-a'] } });
+ assert.equal(keyResponse.status, 201);
+ const key = await keyResponse.json();
+ const viewer = await f.open(await f.issue({}, 'Bearer ' + key.key));
+ const entered = Promise.withResolvers(), released = Promise.withResolvers();
+ f.state.assetEntered = entered.resolve; f.state.assetBarrier = released.promise;
+ const pending = f.request(viewer.base + 'core/rfb.js', { headers: { cookie: viewer.cookie, 'if-none-match': '*' } });
+ try {
+ await entered.promise;
+ const revoked = await f.request('/v1/keys/' + key.id, { method: 'DELETE', headers: { authorization: f.authorization } });
+ assert.equal(revoked.status, 200);
+ } finally { released.resolve(); }
+ const response = await pending;
+ assert.equal(response.status, 401); assert.equal(response.headers.get('etag'), null);
+ assert.equal(response.headers.get('cache-control'), 'no-store');
+ record('assets', t.name, { status: response.status, noEtag: true, noStore: true });
+});
+
+test('application cookies cannot authenticate another lane or desktop; foreign WS origin is rejected', async t => {
+ const f = await fixture(t);
+ const app = await f.open(await f.issue({ target: { type: 'application', application_id: '0x200004' } }));
+ const other = await f.open(await f.issue({ target: { type: 'application', application_id: '0x200005' } }));
+ const first = await f.request(app.base + 'core/rfb.js', { headers: { cookie: app.cookie } });
+ const etag = first.headers.get('etag'); await first.arrayBuffer();
+ for (const base of ['/desktop/', other.base]) assert.equal((await f.request(base + 'core/rfb.js', { headers: { cookie: app.cookie, 'if-none-match': etag ?? '*' } })).status, 401);
+ const settings = await (await f.request(app.base + 'mandatory.json', { headers: { cookie: app.cookie } })).json();
+ assert.equal(settings.path, app.base.slice(1) + 'socket');
+ await f.upgrade(app, 403, 'https://foreign.invalid');
+ assert.equal(f.counts.upstream, 0);
+ await f.upgrade(app);
+ record('admissions', t.name, { crossLaneStatus: 401, desktopStatus: 401, foreignOriginStatus: 403, applicationUpgradeStatus: 101 });
+});
+
+test.after(async () => {
+ if (!process.env.NODE_AGENT_DESKTOP_EVIDENCE_DIR) return;
+ for (const [filename, rows] of [['ticket-flow.json', evidence.admissions], ['cache-headers.json', evidence.assets], ['cache-renewal.json', evidence.renewals], ['cleanup.json', evidence.cleanup]]) {
+ await writeFile(path.join(process.env.NODE_AGENT_DESKTOP_EVIDENCE_DIR, filename), JSON.stringify(rows, null, 2) + '\n', { mode: 0o600 });
+ }
+});
diff --git a/tests/node-agent-embedded-viewer.test.mjs b/tests/node-agent-embedded-viewer.test.mjs
new file mode 100644
index 0000000..d984840
--- /dev/null
+++ b/tests/node-agent-embedded-viewer.test.mjs
@@ -0,0 +1,78 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { mkdtemp, writeFile, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { createHash } from 'node:crypto';
+import { loadEmbeddedViewer } from '../tools/node-agent-api/viewer.mjs';
+import { observeFirstFrame } from '../tools/node-agent-api/web/viewer-frame.js';
+
+async function artifacts(t) {
+ const directory = await mkdtemp(path.join(tmpdir(), 'embedded-viewer-'));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ const body = Buffer.from('export const fixture = 1;');
+ const file = 'viewer-' + createHash('sha256').update(body).digest('hex') + '.js';
+ await writeFile(path.join(directory, file), body);
+ await writeFile(path.join(directory, 'manifest.json'), JSON.stringify({ schema: 1, file, novncVersion: 'fixture' }));
+ return { directory, file, body };
+}
+
+test('embedded viewer serves one hashed static resource and lane-specific authoritative configuration', async t => {
+ const { directory, file, body } = await artifacts(t);
+ const viewer = loadEmbeddedViewer({ directory });
+ const html = viewer.html({ mode: 'view', expiresAt: 100000, socketPath: 'desktop/lane/fixture/socket' });
+ assert.equal((html.match(/src=/g) ?? []).length, 1);
+ assert.ok(html.includes('./' + file));
+ const configuration = JSON.parse(/type="application\/json">(.*?)<\/script>/.exec(html)[1]);
+ assert.deepEqual(configuration, { mode: 'view', expiresAt: 100000, socketPath: '/desktop/lane/fixture/socket' });
+ assert.deepEqual(viewer.asset(file).body, body);
+ assert.equal(viewer.asset('../' + file), null);
+ assert.equal(viewer.asset(file).cacheControl, 'private, max-age=31536000, immutable');
+ assert.throws(() => viewer.html({ mode: 'view', expiresAt: 1, socketPath: '//other.invalid/socket' }));
+ assert.throws(() => viewer.html({ mode: 'admin', expiresAt: 1, socketPath: 'desktop/lane/fixture/socket' }));
+});
+
+test('invalid or changed embedded build artifact fails checksum validation', async t => {
+ const { directory, file } = await artifacts(t);
+ await writeFile(path.join(directory, file), 'changed');
+ assert.throws(() => loadEmbeddedViewer({ directory }), /checksum/);
+});
+
+test('missing build artifacts preserve fallback', async t => {
+ const directory = await mkdtemp(path.join(tmpdir(), 'embedded-viewer-empty-'));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ assert.equal(loadEmbeddedViewer({ directory }), null);
+});
+
+function frameFixture() {
+ const tasks = new Map(), reports = [];
+ let sequence = 0, pending = false, rectResult = true, updateResult = true;
+ const rfb = { _fb_width: 32, _fb_height: 24, _FBU: { encoding: 0, width: 32, height: 24 }, _display: { pending: () => pending }, _handleRect() { return rectResult; }, _framebufferUpdate() { this._handleRect(); return updateResult; } };
+ const scheduler = { requestFrame(fn) { tasks.set(++sequence, fn); return sequence; }, cancelFrame(id) { tasks.delete(id); } };
+ const stop = observeFirstFrame(rfb, value => reports.push(value), scheduler);
+ const tick = () => { const wave = [...tasks.values()]; tasks.clear(); for (const task of wave) task(); };
+ return { rfb, stop, tick, reports, setPending(value) { pending = value; }, setResults(rect, update) { rectResult = rect; updateResult = update; } };
+}
+
+test('first-frame excludes pseudo rectangles and incomplete updates, waits for display flush and reports once', () => {
+ const f = frameFixture();
+ f.rfb._FBU.encoding = -223; f.rfb._framebufferUpdate(); f.tick();
+ assert.deepEqual(f.reports, []);
+ f.rfb._FBU.encoding = 7; f.setResults(false, false); f.rfb._framebufferUpdate(); f.tick();
+ assert.deepEqual(f.reports, []);
+ f.setResults(true, false); f.rfb._framebufferUpdate(); f.tick();
+ assert.deepEqual(f.reports, []);
+ f.setResults(true, true); f.setPending(true); f.rfb._framebufferUpdate(); f.tick();
+ assert.deepEqual(f.reports, []);
+ f.setPending(false); f.tick();
+ assert.deepEqual(f.reports, [{ width: 32, height: 24 }]);
+ f.rfb._framebufferUpdate(); f.tick();
+ assert.equal(f.reports.length, 1);
+ f.stop();
+});
+
+test('disposal cancels outstanding first-frame reports', () => {
+ const f = frameFixture();
+ f.rfb._framebufferUpdate(); f.stop(); f.tick();
+ assert.deepEqual(f.reports, []);
+});
diff --git a/tests/node-agent-performance.test.mjs b/tests/node-agent-performance.test.mjs
new file mode 100644
index 0000000..a5792b2
--- /dev/null
+++ b/tests/node-agent-performance.test.mjs
@@ -0,0 +1,31 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import path from 'node:path';
+import { summarize, runWorker } from '../tools/node-agent-api/bench/performance.mjs';
+
+const sourceRoot = process.env.NODE_AGENT_BENCH_SOURCE_ROOT ?? path.resolve(import.meta.dirname, '..');
+test('nearest-rank summaries do not fabricate missing observations', () => {
+ assert.equal(summarize([]).localDispatchMs.p95, null);
+ assert.equal(summarize(Array.from({ length: 100 }, (_, i) => ({ localDispatchMs: i + 1 }))).localDispatchMs.p95, 95);
+});
+test('real HTTP/Harness/store round trip persists before SSE and dispatches once', async () => {
+ const oldHome = process.env.HOME;
+ const result = await runWorker({ sourceRoot, warmup: 0, samples: 1, scenario: 'replay' });
+ assert.deepEqual(result.failures, []);
+ const row = result.rows[0];
+ assert.equal(row.durableBeforeObserved, true); assert.equal(row.exactlyOneTurn, true);
+ assert.ok(row.counters.storeWrites > 0); assert.equal(row.counters.realDockerCalls, 0);
+ assert.equal(result.cleanup.children, 0); assert.equal(result.cleanup.stateRemoved, true);
+ assert.deepEqual(result.cleanup.credentialAudit.violations, []);
+ assert.ok(result.cleanup.credentialAudit.symlinksChecked >= 1);
+ assert.equal(process.env.HOME, oldHome);
+});
+test('injected Harness reply delay belongs after local dispatch', async () => {
+ const result = await runWorker({ sourceRoot, harnessDelayMs: 80, warmup: 0, samples: 1 });
+ assert.deepEqual(result.failures, []);
+ const row = result.rows[0]; assert.ok(row.httpAcceptedMs - row.localDispatchMs >= 70);
+});
+test('stalled Harness turn has unknown outcome and is never replayed', async () => {
+ const result = await runWorker({ sourceRoot, stallMethod: 'turn/start', timeoutMs: 200, samples: 0, warmup: 0, scenario: 'timeout' });
+ assert.deepEqual(result.rows, [{ unknown: true, turnStart: 1 }]);
+});
diff --git a/tests/node-agent-shared-runtime-performance.test.mjs b/tests/node-agent-shared-runtime-performance.test.mjs
new file mode 100644
index 0000000..85f7d7d
--- /dev/null
+++ b/tests/node-agent-shared-runtime-performance.test.mjs
@@ -0,0 +1,131 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { createServer } from 'node:http';
+import { mkdtemp, realpath, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { createRequire } from 'node:module';
+import { runInNewContext } from 'node:vm';
+import { createSharedRuntime } from '../tools/node-agent-api/shared-runtime.mjs';
+
+const require = createRequire(import.meta.url);
+
+async function fixture(t) {
+ const stateRoot = await realpath(await mkdtemp(join(tmpdir(), 'shared-snapshot-')));
+ const state = { id: 'fixture-container', started: 'start-1', running: true, roster: true, display: 2, assignment: 2, token: 'fixture-private-token', fileToken: 'fixture-private-token', ready: true, fail: false, prepareFail: false };
+ const counts = { inspect: 0, exec: 0, ensure: 0, version: 0, preparation: 0, methods: [], containers: [] };
+ const server = createServer(async (req, res) => {
+ const method = req.url.split('/').at(-1); counts.methods.push(method);
+ let body = ''; for await (const chunk of req) body += chunk;
+ assert.equal(req.headers.authorization, 'Bearer fixture-gateway-token');
+ if (state.fail) { res.writeHead(503); res.end('{}'); return; }
+ if (method === 'ensureForeverBox') {
+ counts.ensure++;
+ await new Promise(resolve => setTimeout(resolve, 20));
+ if (state.prepareFail) { res.writeHead(503); res.end('{}'); return; }
+ state.ready = true;
+ }
+ const result = method === 'listAgents' ? (state.roster ? [{ id: 'bot-a' }] : []) : method === 'getForeverBoxStatus' ? { state: state.ready ? 'running' : 'stopped', vncUrl: `http://127.0.0.1:6081/vnc.html?path=${encodeURIComponent('websockify?token=' + state.display)}` } : {};
+ res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(result));
+ });
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ const runtime = await createSharedRuntime({ stateRoot, spawnProcess: () => { throw new Error('Injected execute must not spawn Docker'); }, execute: async (_file, args) => {
+ if (args[0] === 'inspect') { counts.inspect++; return { stdout: JSON.stringify([{ Id: state.id, Name: '/grok-node-local-vm', State: { Running: state.running, StartedAt: state.started } }]) }; }
+ counts.exec++;
+ counts.containers.push(args[1]);
+ assert.ok([state.id, 'grok-node-local-vm'].includes(args[1]));
+ if (args.includes('--version')) { counts.version++; return { stdout: 'codex-cli 0.160.0' }; }
+ const index = args.indexOf('-e'); assert.ok(index > 0);
+ const program = args[index + 1];
+ if (program.includes('executor_private_home_ready')) { counts.preparation++; return { stdout: 'executor_private_home_ready' }; }
+ const outputs = [], processStub = { argv: ['node', ...args.slice(index + 2).filter((arg, i) => !(i === 0 && arg === '--'))], exitCode: 0, exit(code) { throw new Error('probe exit ' + code); } };
+ await runInNewContext(program, {
+ require(name) {
+ if (name === 'node:http' || name === 'http') {
+ const real = require('node:http');
+ return { ...real, request: (options, cb) => real.request(Number(options?.port) === 1340 ? { ...options, port: server.address().port } : options, cb) };
+ }
+ if (!['fs', 'node:fs'].includes(name)) return require(name);
+ const files = { readFileSync(file) {
+ if (file.endsWith('gateway.json')) return JSON.stringify({ token: 'fixture-gateway-token' });
+ if (file.endsWith('.sand-window-assignments.json')) return JSON.stringify({ assignments: { 'bot-a': state.assignment }, tokens: { 'bot-a': state.token } });
+ if (file.startsWith('/tmp/sand-window-tokens.d/')) return state.fileToken;
+ throw new Error('Unexpected fixture file');
+ } };
+ return { ...files, constants: { O_RDONLY: 0, O_NONBLOCK: 0 }, openSync: file => ({ bytes: Buffer.from(files.readFileSync(file)), offset: 0 }), fstatSync: () => ({ isFile: () => true }), closeSync() {}, readSync(fd, buffer, offset, length) { const count = fd.bytes.copy(buffer, offset, fd.offset, fd.offset + length); fd.offset += count; return count; } };
+ },
+ process: processStub, URL, URLSearchParams, AbortSignal, Buffer,
+ fetch: (url, options) => fetch(String(url).replace('http://127.0.0.1:1340', 'http://127.0.0.1:' + server.address().port), options),
+ console: { log: value => outputs.push(String(value)), error() {} },
+ });
+ if (processStub.exitCode) throw new Error('Gateway probe failed');
+ return { stdout: outputs.join('\n') };
+ } });
+ t.after(async () => { await runtime.close(); await new Promise(resolve => server.close(resolve)); await rm(stateRoot, { recursive: true, force: true }); });
+ return { runtime, state, counts };
+}
+
+test('healthy snapshot executes existing HTTP methods once and never prepares', async t => {
+ const { runtime, counts } = await fixture(t);
+ const result = await runtime.desktop('bot-a', { type: 'desktop' }, 'view', { prepare: true });
+ assert.equal(result.display, 2);
+ assert.equal(result.containerStartedAt, 'start-1');
+ assert.match(result.assignmentRevision, /^[a-f0-9]{64}$/);
+ assert.equal(JSON.stringify(result).includes('fixture-private-token'), false);
+ assert.deepEqual(counts.methods, ['listAgents', 'getForeverBoxStatus']);
+ assert.deepEqual(counts.containers, ['fixture-container']);
+ assert.equal(counts.inspect, 1); assert.equal(counts.exec, 1); assert.equal(counts.ensure, 0);
+});
+
+test('fresh executor identity changes for token, assignment, replacement and same-ID restart', async t => {
+ const { runtime, state } = await fixture(t);
+ const first = await runtime.validateExecutor('bot-a');
+ state.token = state.fileToken = 'rotated-fixture-token';
+ const rotated = await runtime.validateExecutor('bot-a'); assert.notEqual(first.assignmentRevision, rotated.assignmentRevision);
+ state.display = state.assignment = 3;
+ const reassigned = await runtime.validateExecutor('bot-a'); assert.equal(reassigned.display, 3); assert.notEqual(rotated.assignmentRevision, reassigned.assignmentRevision);
+ state.started = 'start-2'; assert.equal((await runtime.validateExecutor('bot-a')).containerStartedAt, 'start-2');
+ state.id = 'fixture-replacement'; assert.equal((await runtime.validateExecutor('bot-a')).containerId, state.id);
+ state.running = false; await assert.rejects(runtime.validateExecutor('bot-a'), { code: 'execution_unavailable' });
+});
+
+test('deleted bot, stale assignment/token and transport errors fail closed without prepare', async t => {
+ const { runtime, state, counts } = await fixture(t);
+ for (const [field, value] of [['roster', false], ['assignment', 7], ['fileToken', 'stale'], ['fail', true]]) {
+ const previous = state[field]; state[field] = value;
+ await assert.rejects(runtime.desktop('bot-a', { type: 'desktop' }, 'view', { prepare: true }));
+ state[field] = previous;
+ }
+ assert.equal(counts.ensure, 0);
+});
+
+test('tokenless primary seat validates without a token file', async t => {
+ const { runtime, state } = await fixture(t);
+ state.token = undefined; state.fileToken = undefined;
+ const result = await runtime.validateExecutor('bot-a');
+ assert.equal(result.display, 2);
+ assert.match(result.assignmentRevision, /^[a-f0-9]{64}$/);
+ assert.equal(JSON.stringify(result).includes('fixture-private-token'), false);
+});
+
+test('only explicit unavailable issue prepares, concurrent failures clear the single flight for retry', async t => {
+ const { runtime, state, counts } = await fixture(t);
+ state.ready = false;
+ await assert.rejects(runtime.desktop('bot-a'), { code: 'desktop_unavailable' }); assert.equal(counts.ensure, 0);
+ state.prepareFail = true;
+ const issue = () => runtime.desktop('bot-a', { type: 'desktop' }, 'view', { prepare: true });
+ const failed = await Promise.allSettled([issue(), issue(), issue()]);
+ assert.equal(failed.every(result => result.status === 'rejected'), true); assert.equal(counts.ensure, 1);
+ state.prepareFail = false;
+ const ready = await Promise.all([issue(), issue(), issue()]);
+ assert.equal(ready.every(result => result.display === 2), true); assert.equal(counts.ensure, 2);
+});
+
+test('executor preparation is shared and bound to assignment and container start', async t => {
+ const { runtime, state, counts } = await fixture(t);
+ await Promise.all([runtime.ensure('bot-a'), runtime.ensure('bot-a')]);
+ assert.equal(counts.version, 1); assert.equal(counts.preparation, 1); assert.equal(counts.ensure, 0);
+ await runtime.ensure('bot-a'); assert.equal(counts.preparation, 1);
+ state.started = 'start-2'; await runtime.ensure('bot-a'); assert.equal(counts.preparation, 2);
+ state.token = state.fileToken = 'new-token'; await runtime.ensure('bot-a'); assert.equal(counts.preparation, 3);
+});
diff --git a/tests/node-agent-snapshot-bridge.test.mjs b/tests/node-agent-snapshot-bridge.test.mjs
new file mode 100644
index 0000000..41638b2
--- /dev/null
+++ b/tests/node-agent-snapshot-bridge.test.mjs
@@ -0,0 +1,187 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { once } from 'node:events';
+import { createServer } from 'node:http';
+import { mkdtemp, realpath, writeFile, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { createSharedRuntime } from '../tools/node-agent-api/shared-runtime.mjs';
+import { createSnapshotBridge, snapshotBridgeProgram } from '../tools/node-agent-api/snapshot-bridge.mjs';
+
+async function fixture(t) {
+ const root = await realpath(await mkdtemp(join(tmpdir(), 'snapshot-bridge-')));
+ const state = { id: 'container-one', started: 'start-one', running: true, display: 2, token: 'private-assignment', gatewayToken: 'private-gateway', ready: true, roster: true, inspectFail: false, statusHook: null };
+ const children = [], exits = [], calls = [], counts = { inspect: 0 };
+ const save = async () => {
+ await writeFile(join(root, 'gateway.json'), JSON.stringify({ token: state.gatewayToken }));
+ await writeFile(join(root, 'assignments.json'), JSON.stringify({ assignments: { bot: state.display }, tokens: { bot: state.token } }));
+ await writeFile(join(root, String(state.display)), state.token);
+ };
+ await save();
+ const server = createServer(async (req, res) => {
+ const method = req.url.split('/').at(-1); calls.push(method);
+ assert.equal(req.headers.authorization, 'Bearer ' + state.gatewayToken);
+ for await (const _chunk of req) { /* consume the read-only request */ }
+ if (method === 'getForeverBoxStatus') await state.statusHook?.();
+ const data = method === 'listAgents' ? (state.roster ? [{ id: 'bot' }] : []) : { state: state.ready ? 'running' : 'stopped', vncUrl: 'http://127.0.0.1:6081/vnc.html?path=' + encodeURIComponent('websockify?token=' + state.display) };
+ res.end(JSON.stringify(data));
+ });
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ const transform = program => program.replaceAll('/home/box/sand-data/gateway.json', join(root, 'gateway.json')).replaceAll('/home/box/.sand-window-assignments.json', join(root, 'assignments.json')).replaceAll('/tmp/sand-window-tokens.d/', root + '/').replace('port:1340', 'port:' + server.address().port);
+ const spawnSnapshot = (_file, args, options) => {
+ assert.deepEqual(args.slice(0, 3), ['exec', '-i', state.id]);
+ for (const flag of ['--reuid=box', '--regid=box', '--init-groups', '--bounding-set=-all', '--no-new-privs']) assert.ok(args.includes(flag));
+ assert.equal(options.stdio[2], 'ignore');
+ const program = args[args.indexOf('-e') + 1];
+ const child = spawn(process.execPath, ['-e', transform(program)], options);
+ children.push(child); exits.push(once(child, 'close'));
+ return child;
+ };
+ const runtime = await createSharedRuntime({ stateRoot: join(root, 'runtime'), spawnSnapshot, execute: async (_file, args) => {
+ assert.equal(args[0], 'inspect'); counts.inspect++;
+ if (state.inspectFail) throw new Error('inspect failed');
+ return { stdout: JSON.stringify([{ Id: state.id, Name: '/grok-node-local-vm', State: { Running: state.running, StartedAt: state.started } }]) };
+ } });
+ t.after(async () => { await runtime.close(); await Promise.all(exits); await new Promise(resolve => server.close(resolve)); await rm(root, { recursive: true, force: true }); });
+ return { runtime, state, root, save, children, exits, calls, counts, transform };
+}
+
+test('persistent lowpriv bridge rereads assignments, token hash and authenticated gateway on every call', async t => {
+ const f = await fixture(t);
+ const first = await f.runtime.desktop('bot');
+ const same = await f.runtime.validateDesktop('bot', first);
+ assert.equal(same.assignmentRevision, first.assignmentRevision);
+ f.state.token = 'rotated-assignment'; f.state.gatewayToken = 'rotated-gateway'; await f.save();
+ await assert.rejects(f.runtime.validateDesktop('bot', first), { code: 'desktop_changed' });
+ const fresh = await f.runtime.desktop('bot');
+ assert.notEqual(fresh.assignmentRevision, first.assignmentRevision);
+ assert.equal(JSON.stringify(fresh).includes('rotated-'), false);
+ assert.equal(f.children.length, 1);
+ assert.equal(f.counts.inspect, 8);
+ assert.deepEqual(f.calls, Array.from({ length: 4 }, () => ['listAgents', 'getForeverBoxStatus']).flat());
+ f.state.ready = false;
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'desktop_unavailable' });
+ f.state.roster = false;
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'not_found' });
+ assert.equal(f.calls.includes('ensureForeverBox'), false);
+});
+
+test('fresh inspect replaces the bridge for same-ID restart and container replacement', async t => {
+ const f = await fixture(t), first = await f.runtime.desktop('bot');
+ f.state.started = 'start-two';
+ await assert.rejects(f.runtime.validateDesktop('bot', first), { code: 'desktop_changed' });
+ await f.exits[0];
+ assert.equal(f.children[0].killed, true);
+ f.state.id = 'container-two';
+ const fresh = await f.runtime.desktop('bot');
+ assert.equal(fresh.containerId, 'container-two');
+ assert.equal(f.children.length, 3);
+});
+
+test('post-snapshot inspect rejects a restart during the gateway response', async t => {
+ const f = await fixture(t);
+ f.state.statusHook = () => { f.state.started = 'changed-in-flight'; };
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'desktop_changed' });
+ await f.exits[0];
+ assert.equal(f.children[0].killed, true);
+});
+
+test('stopped and uninspectable containers invalidate live bridge and never return old snapshots', async t => {
+ const f = await fixture(t);
+ await f.runtime.desktop('bot'); f.state.running = false;
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'execution_unavailable' });
+ await f.exits[0];
+ f.state.running = true; await f.runtime.desktop('bot'); f.state.inspectFail = true;
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'execution_unavailable' });
+ await f.exits[1];
+ assert.equal(f.children.length, 2);
+});
+
+test('crash rejects an in-flight request and a later call obtains a fresh bridge', async t => {
+ const f = await fixture(t);
+ await f.runtime.desktop('bot');
+ let signal;
+ const entered = new Promise(resolve => { signal = resolve; });
+ f.state.statusHook = () => { signal(); f.children[0].kill('SIGKILL'); };
+ const pending = f.runtime.desktop('bot');
+ const rejection = assert.rejects(pending, { code: 'upstream_unavailable' });
+ await entered; await rejection; await f.exits[0];
+ f.state.statusHook = null;
+ assert.equal((await f.runtime.desktop('bot')).display, 2);
+ assert.equal(f.children.length, 2);
+});
+
+test('stale token and oversized assignment files fail closed without exposing file contents', async t => {
+ const f = await fixture(t);
+ await writeFile(join(f.root, '2'), 'wrong-token');
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'upstream_unavailable' });
+ await f.save();
+ await writeFile(join(f.root, 'assignments.json'), ' '.repeat(1048577));
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'upstream_unavailable' });
+});
+
+test('runtime.close kills the persistent process and rejects subsequent validation', async t => {
+ const f = await fixture(t);
+ await f.runtime.desktop('bot');
+ await f.runtime.close(); await f.exits[0];
+ assert.equal(f.children[0].killed, true);
+ await assert.rejects(f.runtime.desktop('bot'), { code: 'execution_unavailable' });
+});
+
+test('concurrent snapshots correlate distinct responses on a single process', async t => {
+ const f = await fixture(t);
+ const results = await Promise.all(Array.from({ length: 8 }, () => f.runtime.desktop('bot')));
+ assert.equal(results.every(result => result.display === 2), true);
+ assert.equal(f.children.length, 1);
+ assert.equal(f.calls.filter(method => method === 'getForeverBoxStatus').length, 8);
+ assert.equal(f.counts.inspect, 16);
+});
+
+function transport(t, program, options = {}) {
+ const children = [], exits = [];
+ const bridge = createSnapshotBridge({ docker: process.execPath, spawnSnapshot: (file, args, spawnOptions) => {
+ const child = spawn(file, args, spawnOptions); children.push(child); exits.push(once(child, 'close')); return child;
+ }, argsFor: () => ['-e', program], ...options });
+ t.after(async () => { bridge.close(); await Promise.all(exits); });
+ return { bridge, children, exits, row: { Id: 'id', State: { Running: true, StartedAt: 'start' } } };
+}
+
+test('request capacity rejects excess work and timeout kills the hung bridge', async t => {
+ const f = transport(t, 'process.stdin.resume()', { maxPending: 1, timeoutMs: 100 });
+ const pending = f.bridge.read('bot', f.row);
+ const rejection = assert.rejects(pending, { code: 'snapshot_unavailable' });
+ await assert.rejects(f.bridge.read('bot', f.row), { code: 'snapshot_capacity' });
+ await rejection; await f.exits[0];
+ assert.equal(f.children[0].killed, true);
+});
+
+test('closing the bridge rejects every pending request and prevents respawn', async t => {
+ const f = transport(t, 'process.stdin.resume()');
+ const rejected = [f.bridge.read('bot', f.row), f.bridge.read('other', f.row)].map(pending => assert.rejects(pending, { code: 'snapshot_unavailable' }));
+ f.bridge.close();
+ await Promise.all(rejected); await f.exits[0];
+ await assert.rejects(f.bridge.read('bot', f.row), { code: 'snapshot_unavailable' });
+ assert.equal(f.children.length, 1);
+});
+
+test('oversized and malformed bridge output fail closed', async t => {
+ for (const output of ['x'.repeat(65), 'not-json\n', '{"id":900,"result":{}}\n']) {
+ const f = transport(t, 'process.stdin.once("data",()=>process.stdout.write(' + JSON.stringify(output) + '));process.stdin.resume()', { maxBytes: 64 });
+ await assert.rejects(f.bridge.read('bot', f.row), { code: 'snapshot_unavailable' });
+ await f.exits[0]; assert.equal(f.children[0].killed, true);
+ }
+});
+
+test('wire accepts no arbitrary command or gateway method and bounds input size', async t => {
+ for (const payload of ['{"id":1,"botId":"bot","command":"touch forbidden"}\n', '{"id":1,"botId":"bot","method":"ensureForeverBox"}\n', 'x'.repeat(16385)]) {
+ const child = spawn(process.execPath, ['-e', snapshotBridgeProgram], { stdio: ['pipe', 'pipe', 'pipe'] });
+ const closed = once(child, 'close'); let stdout = '', stderr = '';
+ child.stdout.on('data', bytes => { stdout += bytes; }); child.stderr.on('data', bytes => { stderr += bytes; });
+ child.stdin.on('error', () => {});
+ t.after(() => child.kill('SIGKILL'));
+ child.stdin.write(payload);
+ const [code] = await closed;
+ assert.equal(code, 1); assert.equal(stdout, ''); assert.equal(stderr, '');
+ }
+});
diff --git a/tests/node-agent-store-commit.test.mjs b/tests/node-agent-store-commit.test.mjs
new file mode 100644
index 0000000..2b3168b
--- /dev/null
+++ b/tests/node-agent-store-commit.test.mjs
@@ -0,0 +1,239 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs/promises';
+import { readFileSync } from 'node:fs';
+import { syncBuiltinESMExports } from 'node:module';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+import { createStore } from '../tools/node-agent-api/store.mjs';
+import { ApiError } from '../tools/node-agent-api/errors.mjs';
+import { startNodeAgentApi } from '../tools/node-agent-api/server.mjs';
+
+async function evidence(name, value) {
+ const dir = process.env.NODE_AGENT_STORE_EVIDENCE_DIR;
+ if (!dir) return;
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
+ await fs.writeFile(join(dir, name + '.json'), JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
+}
+
+async function fixture(t) {
+ const dir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'node-store-commit-')));
+ t.after(() => fs.rm(dir, { recursive: true, force: true }));
+ const store = await createStore(dir);
+ const a = (await store.create('a')).session, b = (await store.create('b')).session;
+ return { dir, file: join(dir, 'sessions.json'), store, a, b };
+}
+
+function replaceFs(t, name, implementation) {
+ const original = fs[name];
+ fs[name] = implementation;
+ syncBuiltinESMExports();
+ t.after(() => { fs[name] = original; syncBuiltinESMExports(); });
+}
+
+const delta = n => ({ type: 'node.harness.event', data: { sequence: n } });
+
+test('commit patches and interleaved batch events persist once before publishing stable sessions', async t => {
+ const { store, file, a, b } = await fixture(t);
+ let writes = 0, guards = 0;
+ const rename = fs.rename;
+ replaceFs(t, 'rename', async (...args) => { writes++; return rename(...args); });
+ store.setWriteGuard(async () => { guards++; });
+ const emitted = [], local = [];
+ store.subscribe(a.id, event => local.push(event));
+ store.subscribeAll(event => {
+ const disk = JSON.parse(readFileSync(file, 'utf8'));
+ assert.deepEqual(disk, store.list());
+ assert.equal(store.get(a.id), a);
+ assert.equal(store.get(b.id), b);
+ emitted.push(event);
+ });
+ const single = await store.commit(a.id, { changes: { task_status: 'running' }, events: [delta(0)] });
+ assert.equal(writes, 1); assert.equal(guards, 1);
+ assert.equal(single.length, 1); assert.equal(single[0], emitted[0]);
+ const batch = await store.commitBatch([
+ { id: a.id, changes: { metadata: { first: true } }, events: [delta(1), delta(2)] },
+ { id: b.id, changes: { task_status: 'done' }, events: [delta(3)] },
+ { id: a.id, changes: { task_status: 'done' }, events: [delta(4)] },
+ ]);
+ assert.equal(writes, 2); assert.equal(guards, 2);
+ assert.deepEqual(batch.map(e => e.data.sequence), [1, 2, 3, 4]);
+ assert.deepEqual(emitted, [...single, ...batch]);
+ assert.deepEqual(local.map(e => e.data.sequence), [0, 1, 2, 4]);
+ assert.deepEqual(a.metadata, { first: true }); assert.equal(a.task_status, 'done');
+ const legacy = await store.append(a.id, 'legacy', { ok: true });
+ assert.equal(legacy, a.events.at(-1));
+ assert.equal(await store.update(a.id, { status: 'idle' }), a);
+ assert.equal(writes, 4); assert.equal(guards, 4);
+ assert.deepEqual(await store.commitBatch([]), []); assert.equal(writes, 4);
+ await evidence('commit-counts', { singleWrites: 1, batchWrites: 1, batchEvents: 4, legacyWrites: 2, totalWrites: writes, guards, sequence: batch.map(e => e.data.sequence), stableReferences: true, durableBeforePublish: true });
+});
+
+test('queued commits clone inputs and flush waits for the persistence barrier', async t => {
+ const { store, file, a } = await fixture(t);
+ const before = store.snapshot();
+ const entered = Promise.withResolvers(), release = Promise.withResolvers();
+ store.setWriteGuard(async () => { entered.resolve(); await release.promise; });
+ const operations = [{ id: a.id, changes: { metadata: { value: 'original' } }, events: [{ type: 'first', data: { value: 'original' } }] }];
+ let emits = 0, flushed = false;
+ store.subscribeAll(() => emits++);
+ const first = store.commitBatch(operations);
+ await entered.promise;
+ operations[0].changes.metadata.value = 'mutated'; operations[0].events[0].data.value = 'mutated';
+ operations.push({ id: a.id, events: [delta(9)] });
+ const second = store.commit(a.id, { changes: { task_status: 'done' }, events: [delta(2)] });
+ const flush = store.flush().then(() => { flushed = true; });
+ await new Promise(resolve => setImmediate(resolve));
+ assert.equal(store.snapshot(), before); assert.equal(await fs.readFile(file, 'utf8'), before);
+ assert.equal(emits, 0); assert.equal(flushed, false);
+ release.resolve();
+ await Promise.all([first, second, flush]);
+ assert.equal(a.metadata.value, 'original'); assert.equal(a.events[1].data.value, 'original');
+ assert.deepEqual(a.events.slice(1).map(e => e.type), ['first', 'node.harness.event']);
+ assert.equal(emits, 2); assert.equal(flushed, true);
+ await evidence('queue-barrier', { unpublishedWhileBlocked: true, clonedInputs: true, events: 2, flushDrained: true });
+});
+
+test('later missing session, invalid arguments and aggregate event cap discard the whole candidate', async t => {
+ const { store, file, a, b } = await fixture(t);
+ let writes = 0, emits = 0;
+ store.setWriteGuard(async () => { writes++; }); store.subscribeAll(() => emits++);
+ const before = store.snapshot();
+ const invalid = [null, {}, [null], [{ id: '' }], [{ id: 1 }], [{ id: a.id, events: {} }], [{ id: a.id, events: [null] }], [{ id: a.id, events: [{ type: 1 }] }], [{ id: a.id, events: new Array(1) }], [{ id: a.id, changes: [] }], [{ id: a.id, changes: null }], [{ id: a.id, changes: { id: b.id } }], [{ id: a.id, changes: { events: [] } }]];
+ for (const operations of invalid) await assert.rejects(store.commitBatch(operations));
+ for (const patch of [null, [], 1]) await assert.rejects(store.commit(a.id, patch), { code: 'invalid_request' });
+ await assert.rejects(store.commitBatch([{ id: a.id, changes: { metadata: { leak: true } }, events: [delta(1)] }, { id: 'missing', events: [delta(2)] }]), { code: 'not_found' });
+ await assert.rejects(store.commitBatch([{ id: a.id, changes: { status: 'closed' }, events: [delta(0)] }, { id: b.id, events: Array.from({ length: 9999 }, (_, i) => delta(i)) }, { id: b.id, events: [delta(10000)] }]), { code: 'event_limit' });
+ await assert.rejects(store.commitBatch([{ id: a.id, changes: { status: 'closed' }, events: [delta(1)] }, { id: b.id, events: [{ type: 'invalid-json', data: 1n }] }]), TypeError);
+ assert.equal(store.snapshot(), before); assert.equal(await fs.readFile(file, 'utf8'), before);
+ assert.equal(writes, 0); assert.equal(emits, 0);
+ await store.commit(b.id, { events: Array.from({ length: 9999 }, (_, i) => delta(i)) });
+ assert.equal(b.events.length, 10000);
+ await assert.rejects(store.append(b.id, 'overflow', {}), { code: 'event_limit' });
+ assert.equal(writes, 1); assert.equal(emits, 9999);
+ await evidence('candidate-rejection', { invalidCases: invalid.length + 3, missingSessionRejected: true, aggregateCapRejected: true, serializationFailureRejected: true, rejectedWrites: 0, rejectedEmits: 0, acceptedEventCount: b.events.length });
+});
+
+test('quota guard rejects a complete candidate without poisoning the next small commit', async t => {
+ const { store, file, a, b } = await fixture(t);
+ const before = store.snapshot(); let guards = 0, emits = 0;
+ store.subscribeAll(() => emits++);
+ store.setWriteGuard(async rows => {
+ guards++;
+ if (Buffer.byteLength(JSON.stringify(rows)) > 2000) throw new ApiError(413, 'storage_quota', 'fixture quota');
+ });
+ await assert.rejects(store.commitBatch([{ id: a.id, events: [delta(1)] }, { id: b.id, changes: { metadata: { oversized: 'x'.repeat(3000) } }, events: [delta(2)] }]), { code: 'storage_quota' });
+ assert.equal(store.snapshot(), before); assert.equal(await fs.readFile(file, 'utf8'), before); assert.equal(emits, 0);
+ await store.commit(a.id, { changes: { metadata: { small: true } }, events: [delta(3)] });
+ await store.flush();
+ assert.equal(guards, 2); assert.equal(emits, 1);
+ assert.deepEqual(JSON.parse(await fs.readFile(file, 'utf8')), store.list());
+ await evidence('quota-retry', { rejectedEmits: 0, rejectedMemoryChanges: 0, guards, acceptedEmits: emits, flushSucceeded: true });
+});
+
+for (const stage of ['file-fsync', 'rename', 'directory-fsync']) {
+ test(`real atomicJson ${stage} fault publishes nothing, stays closed, and reopens a complete snapshot`, async t => {
+ const { store, file, dir, a, b } = await fixture(t);
+ const before = store.snapshot(), failure = Object.assign(new Error('fixture ' + stage), { code: 'EIO' });
+ let hits = 0, emits = 0;
+ store.subscribeAll(() => emits++);
+ if (stage === 'rename') {
+ replaceFs(t, 'rename', async () => { hits++; throw failure; });
+ } else {
+ const open = fs.open;
+ replaceFs(t, 'open', async (path, ...args) => {
+ const handle = await open(path, ...args);
+ if (stage === 'directory-fsync' ? path === dir : String(path).startsWith(file + '.tmp-')) {
+ handle.sync = async () => { hits++; throw failure; };
+ }
+ return handle;
+ });
+ }
+ const changes = [{ id: a.id, changes: { task_status: 'done' }, events: [delta(1)] }, { id: b.id, changes: { task_status: 'done' }, events: [delta(2)] }];
+ await assert.rejects(store.commitBatch(changes), error => error === failure);
+ assert.equal(hits, 1); assert.equal(emits, 0); assert.equal(store.snapshot(), before);
+ await assert.rejects(store.append(a.id, 'retry', {}), error => error === failure);
+ await assert.rejects(store.update(a.id, { status: 'closed' }), error => error === failure);
+ await assert.rejects(store.flush(), error => error === failure);
+ assert.equal(hits, 1);
+ const disk = JSON.parse(await fs.readFile(file, 'utf8'));
+ const afterRename = stage === 'directory-fsync';
+ if (afterRename) {
+ assert.deepEqual(disk.map(row => row.task_status), ['done', 'done']);
+ assert.deepEqual(disk.map(row => row.events.at(-1).data.sequence), [1, 2]);
+ } else assert.equal(JSON.stringify(disk), before);
+ const reopened = await createStore(dir);
+ assert.deepEqual(reopened.list(), disk);
+ await evidence(stage, { hits, emits, memoryUnchanged: true, retryFailedClosed: true, flushFailedClosed: true, reopenedWholeSnapshot: true, disk: afterRename ? 'complete-new-snapshot' : 'complete-old-snapshot' });
+ });
+}
+
+test('real disk opens in a fresh process with original cursor IDs and pending input recovery', async t => {
+ const { store, dir, a, b } = await fixture(t);
+ const events = await store.commitBatch([{ id: a.id, changes: { status: 'pending', requests: { request_fixture: { status: 'pending', fingerprint: 'fixture' } } }, events: [delta(1), delta(2)] }, { id: b.id, events: [delta(3)] }]);
+ await store.flush();
+ const script = `import { createStore } from ${JSON.stringify(new URL('../tools/node-agent-api/store.mjs', import.meta.url).href)};
+ const store = await createStore(process.argv[1]); await store.flush(); console.log(store.snapshot());`;
+ const { stdout } = await promisify(execFile)(process.execPath, ['--input-type=module', '-e', script, dir]);
+ const rows = JSON.parse(stdout), recovered = rows.find(row => row.id === a.id);
+ assert.deepEqual(recovered.events.slice(1, 3), events.slice(0, 2));
+ const replay = recovered.events.slice(recovered.events.findIndex(event => event.id === events[0].id) + 1);
+ assert.equal(replay[0].id, events[1].id); assert.equal(replay[1].type, 'node.session.input.unknown');
+ assert.equal(replay[1].data.replayed, false); assert.equal(recovered.status, 'unknown');
+ assert.equal(recovered.requests.request_fixture.status, 'unknown');
+ const reopened = await createStore(dir);
+ assert.equal(reopened.get(a.id).events.length, recovered.events.length);
+ assert.deepEqual(reopened.get(b.id).events.at(-1), events[2]);
+ await evidence('restart-replay', { freshProcess: true, originalEventIds: events.map(e => e.id), cursor: events[0].id, replayIds: replay.map(e => e.id), recoveryEvents: 1, replayed: false, repeatedOpenAddedEvents: 0 });
+});
+
+test('real HTTP SSE exposes committed envelopes exactly once and resumes from cursor after reopen', async t => {
+ const dir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'node-store-sse-')));
+ let store, service; const readers = [];
+ const adapter = { independentSessions: true, writesEnabled: false, attachStore(value) { store = value; }, agents: async () => [], items: async () => [] };
+ t.after(async () => {
+ for (const reader of readers) await reader.cancel();
+ try { if (service) await service.close(); }
+ finally { await fs.rm(dir, { recursive: true, force: true }); }
+ });
+ service = await startNodeAgentApi({ adapter, stateDirectory: dir });
+ const a = (await store.create('a')).session;
+ const key = (await fs.readFile(service.keyFile, 'utf8')).trim();
+ const route = '/v1/agents/sessions/' + a.id + '/events';
+ async function stream(cursor) {
+ const response = await fetch(service.origin + route, { headers: { authorization: 'Bearer ' + key, accept: 'text/event-stream', 'last-event-id': cursor }, signal: AbortSignal.timeout(10000) });
+ assert.equal(response.status, 200);
+ const reader = response.body.getReader(), decoder = new TextDecoder(); let buffer = '';
+ readers.push(reader);
+ return { reader, async next() {
+ while (!buffer.includes('\n\n')) { const { value, done } = await reader.read(); assert.equal(done, false); buffer += decoder.decode(value, { stream: true }); }
+ const boundary = buffer.indexOf('\n\n'), frame = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2);
+ return JSON.parse(frame.split('\n').find(line => line.startsWith('data: ')).slice(6));
+ } };
+ }
+ const live = await stream(a.events[0].id);
+ const entered = Promise.withResolvers(), release = Promise.withResolvers();
+ store.setWriteGuard(async () => { entered.resolve(); await release.promise; });
+ const write = store.commit(a.id, { changes: { task_status: 'done' }, events: [delta(1), delta(2)] });
+ await entered.promise;
+ let received = false;
+ const first = live.next().then(event => { received = true; return event; });
+ const history = await fetch(service.origin + route, { headers: { authorization: 'Bearer ' + key } });
+ const historyBody = await history.json();
+ assert.deepEqual(historyBody.data.map(e => e.id), [a.events[0].id]); assert.equal(received, false);
+ release.resolve(); const events = await write;
+ assert.deepEqual(await first, events[0]); assert.deepEqual(await live.next(), events[1]);
+ store.setWriteGuard(async () => { throw new ApiError(413, 'storage_quota', 'fixture'); });
+ await assert.rejects(store.commit(a.id, { events: [delta(99)] }), { code: 'storage_quota' });
+ store.setWriteGuard(async () => {});
+ const final = await store.append(a.id, 'sentinel', {});
+ assert.deepEqual(await live.next(), final);
+ await live.reader.cancel(); await service.close(); service = undefined;
+ service = await startNodeAgentApi({ adapter, stateDirectory: dir });
+ const replay = await stream(events[0].id);
+ assert.deepEqual(await replay.next(), events[1]); assert.deepEqual(await replay.next(), final);
+ await replay.reader.cancel();
+ await evidence('sse-replay', { unpublishedWhileBlocked: true, historyCommittedOnly: true, liveIds: [...events, final].map(e => e.id), rejectedEventObserved: false, resumedIds: [events[1].id, final.id] });
+});
diff --git a/tests/node-agent-ui-rendering.test.mjs b/tests/node-agent-ui-rendering.test.mjs
new file mode 100644
index 0000000..07218eb
--- /dev/null
+++ b/tests/node-agent-ui-rendering.test.mjs
@@ -0,0 +1,337 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
+import { createServer } from 'node:http';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { createPaintQueue, createRefresh } from '../tools/node-agent-api/web/client.js';
+
+function clock() {
+ let id = 0;
+ const frames = new Map(), timers = new Map();
+ return {
+ frames, timers, isHidden: () => true,
+ requestFrame: fn => { frames.set(++id, fn); return id; },
+ cancelFrame: id => frames.delete(id),
+ setTimer: fn => { timers.set(++id, fn); return id; },
+ clearTimer: id => timers.delete(id),
+ frame() { const batch = [...frames.values()]; frames.clear(); batch.forEach(fn => fn()); },
+ timer() { const batch = [...timers.values()]; timers.clear(); batch.forEach(fn => fn()); },
+ };
+}
+
+test('5000 deltas in 300 frames paint each dirty item once per frame; flush and reset cancel callbacks', () => {
+ const time = clock(), rows = new Map(), painted = [];
+ const queue = createPaintQueue(() => { painted.push([...rows.values()].join('')); }, time);
+ for (let frame = 0; frame < 300; frame++) {
+ for (let i = Math.floor(frame * 5000 / 300); i < Math.floor((frame + 1) * 5000 / 300); i++) {
+ rows.set('a', (rows.get('a') || '') + '文'); queue.schedule();
+ }
+ assert.equal(time.frames.size, 1); assert.equal(time.timers.size, 1);
+ time.frame();
+ }
+ assert.equal(painted.length, 300); assert.equal(painted.at(-1), '文'.repeat(5000));
+ queue.schedule(); queue.flush(); assert.equal(painted.length, 301);
+ assert.equal(time.frames.size + time.timers.size, 0);
+ queue.schedule(); const stale = [...time.frames.values()][0]; queue.reset(); stale();
+ assert.equal(painted.length, 301);
+ queue.schedule(); time.timer(); assert.equal(painted.length, 302);
+ assert.equal(time.frames.size + time.timers.size, 0);
+});
+
+test('refresh requests coalesce, suppress stale snapshots, and drain trailing dirty updates', async () => {
+ const barriers = [], applied = [];
+ const refresh = createRefresh(async current => {
+ const value = await new Promise(resolve => barriers.push(resolve));
+ if (current()) applied.push(value);
+ });
+ const first = refresh();
+ assert.equal(refresh(), first); assert.equal(refresh(), first);
+ assert.equal(barriers.length, 1);
+ barriers[0]('old'); await new Promise(resolve => setImmediate(resolve));
+ assert.equal(barriers.length, 2); assert.deepEqual(applied, []);
+ refresh(); barriers[1]('middle'); await new Promise(resolve => setImmediate(resolve));
+ assert.equal(barriers.length, 3);
+ barriers[2]('final'); await first; assert.deepEqual(applied, ['final']);
+});
+
+test('reset fences pending refreshes; failed dirty fetch still drains the final request', async () => {
+ const barriers = [], applied = [];
+ const refresh = createRefresh(async current => {
+ const value = await new Promise((resolve, reject) => barriers.push({ resolve, reject }));
+ if (current()) applied.push(value);
+ });
+ const old = refresh(); refresh(); refresh.reset();
+ const next = refresh(); barriers[0].resolve('old session'); await old;
+ barriers[1].resolve('new session'); await next;
+ assert.deepEqual(applied, ['new session']); assert.equal(barriers.length, 2);
+ const failure = refresh(); refresh(); barriers[2].reject(new Error('transient read'));
+ await new Promise(resolve => setImmediate(resolve));
+ barriers[3].resolve('latest'); await failure;
+ assert.deepEqual(applied, ['new session', 'latest']);
+ const finalFailure = refresh(); barriers[4].reject(new Error('final read failed'));
+ await assert.rejects(finalFailure, /final read failed/);
+});
+
+const probe = `
+const p = window.probe = { frame: 0, writes: [], latencies: [], arrivals: {}, parsed: [], formatted: 0, controls: 0, held: [], holdFrames: false, holdTimers: false };
+p.itemsResponses = 0;
+const fetcher = window.fetch.bind(window);
+window.fetch = async (...args) => {
+ const response = await fetcher(...args);
+ if (String(args[0]).includes('/items')) {
+ const json = response.json.bind(response);
+ response.json = async () => { const value = await json(); p.itemsResponses++; return value; };
+ }
+ return response;
+};
+const raf = window.requestAnimationFrame.bind(window), cancel = window.cancelAnimationFrame.bind(window);
+function tick() { p.frame++; raf(tick); } raf(tick);
+window.requestAnimationFrame = fn => p.holdFrames ? (p.held.push(fn), -p.held.length) : raf(fn);
+window.cancelAnimationFrame = id => { if (id >= 0) cancel(id); };
+const timer = window.setTimeout.bind(window);
+window.setTimeout = (fn, delay, ...args) => p.holdTimers && delay === 100 ? (p.held.push(fn), -p.held.length) : timer(fn, delay, ...args);
+const parse = JSON.parse, stringify = JSON.stringify;
+JSON.parse = (...args) => {
+ const value = parse(...args);
+ if (value?.id?.startsWith('evt_')) {
+ p.parsed.push(value.id);
+ const params = value.data?.params;
+ if (params?.delta !== undefined) (p.arrivals[params.itemId] ??= []).push(performance.now());
+ }
+ return value;
+};
+JSON.stringify = (...args) => { if (args[0]?.id?.startsWith('evt_')) p.formatted++; return stringify(...args); };
+const detached = new WeakMap();
+function record(node, value, frame, at) {
+ const label = node.parentElement?.querySelector('strong')?.textContent || '';
+ if (label.startsWith('流式输出')) {
+ const item = label.split(' · ').at(-1);
+ p.writes.push({ item, frame, length: value.length });
+ for (const arrived of p.arrivals[item] || []) p.latencies.push(at - arrived);
+ delete p.arrivals[item];
+ }
+}
+const append = Element.prototype.append;
+Element.prototype.append = function(...nodes) {
+ append.apply(this, nodes);
+ for (const node of nodes) {
+ const row = detached.get(node);
+ if (row) { record(node, ...row); detached.delete(node); }
+ }
+};
+const descriptor = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
+Object.defineProperty(Node.prototype, 'textContent', { ...descriptor, set(value) {
+ if (this.tagName === 'PRE') {
+ if (!this.parentElement) detached.set(this, [value, p.frame, performance.now()]);
+ else record(this, value, p.frame, performance.now());
+ }
+ descriptor.set.call(this, value);
+} });
+const disabled = Object.getOwnPropertyDescriptor(HTMLButtonElement.prototype, 'disabled');
+Object.defineProperty(HTMLButtonElement.prototype, 'disabled', { ...disabled, set(value) { p.controls++; disabled.set.call(this, value); } });
+`;
+
+if (process.env.NODE_AGENT_UI_BROWSER === '1') test('Chromium: burst, terminal, visibility, session epochs, dirty refreshes and cursor recovery', { timeout: 90000 }, async () => {
+ const evidence = process.env.NODE_AGENT_UI_EVIDENCE;
+ assert.ok(evidence); await mkdir(evidence, { recursive: true, mode: 0o700 });
+ const { loadOmowright } = await import(pathToFileURL(process.env.NODE_AGENT_UI_BROWSER_LOADER).href);
+ const { omowright } = await loadOmowright();
+ const web = new URL('../tools/node-agent-api/web/', import.meta.url);
+ const streams = new Map(), requests = [], pending = { items: [], actions: [] };
+ const hold = { items: false, actions: false };
+ let itemRows = [{ id: 'seed', role: 'assistant', text: 'Fixture history' }], actionRows = [];
+ const server = createServer(async (req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ const json = value => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(value)); };
+ if (url.pathname === '/probe.js') { res.writeHead(200, { 'Content-Type': 'text/javascript' }); res.end(probe); return; }
+ if (url.pathname === '/' || /^\/(app\.js|client\.js|style\.css)$/.test(url.pathname)) {
+ const name = url.pathname === '/' ? 'index.html' : url.pathname.slice(1);
+ let body = await readFile(new URL(name, web), 'utf8');
+ if (name === 'index.html') body = body.replace('', '');
+ res.writeHead(200, { 'Content-Type': name.endsWith('.js') ? 'text/javascript' : name.endsWith('.css') ? 'text/css' : 'text/html' }); res.end(body); return;
+ }
+ if (url.pathname === '/favicon.ico') { res.writeHead(204); res.end(); return; }
+ requests.push({ method: req.method, path: url.pathname, cursor: req.headers['last-event-id'] || '', bearer: req.headers.authorization === 'Bearer rendering-fixture' });
+ if (url.pathname === '/v1/capabilities') return json({ writes_enabled: true, events: { sse: true, cancellation: true, required_actions: true } });
+ if (url.pathname === '/v1/agents') return json({ data: [{ id: 'bot', name: 'Rendering fixture' }] });
+ if (url.pathname === '/v1/agents/sessions') return json({ data: ['s1', 's2'].map(id => ({ id, agent_id: 'bot', status: 'idle' })) });
+ const id = url.pathname.split('/')[4], resource = url.pathname.split('/')[5];
+ if (!resource) return json({ id, agent_id: 'bot', status: 'idle' });
+ if (resource === 'items' || resource === 'actions') {
+ if (hold[resource]) { pending[resource].push(rows => json({ data: rows })); return; }
+ return json({ data: resource === 'items' ? itemRows : actionRows });
+ }
+ if (resource === 'events') {
+ assert.equal(req.method, 'GET');
+ res.writeHead(200, { 'Content-Type': 'text/event-stream' }); res.write(': connected\n\n');
+ streams.set(id, res); req.on('close', () => { if (streams.get(id) === res) streams.delete(id); }); return;
+ }
+ res.writeHead(404); res.end();
+ });
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
+ const profile = await mkdtemp(join(tmpdir(), 'ui-rendering-'));
+ let browser, sequence = 0;
+ const observations = [];
+ const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
+ const until = async condition => { for (let i = 0; i < 200; i++) { if (await condition()) return; await pause(20); } assert.fail('fixture condition timed out'); };
+ const emit = (type, data, session = 's1', fragmented = false) => {
+ const id = 'evt_' + ++sequence;
+ const bytes = Buffer.from(`id: ${id}\r\nevent: ${type}\r\ndata: ${JSON.stringify({ id, type, session_id: session, data })}\r\n\r\n`);
+ if (fragmented) for (const byte of bytes) streams.get(session).write(Buffer.of(byte));
+ else streams.get(session).write(bytes);
+ return id;
+ };
+ const delta = (itemId, value, session, fragmented) => emit('node.harness.event', { method: 'item/agentMessage/delta', params: { itemId, delta: value } }, session, fragmented);
+ try {
+ browser = await omowright.connectPipe({ browserPath: process.env.NODE_AGENT_UI_CHROME || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', browserArgs: ['--headless=new', '--no-first-run', `--user-data-dir=${profile}`, '--window-size=1280,900'], storageRoot: profile });
+ const page = await browser.newTab(`http://127.0.0.1:${server.address().port}`);
+ const evaluate = expression => page.evaluate(expression);
+ const wait = expression => until(() => evaluate(expression));
+ await wait("document.getElementById('send').disabled");
+ await page.locator('#key').fill('rendering-fixture'); await page.locator('#connect').click();
+ await wait("document.getElementById('bots').options.length === 2");
+ await page.locator('#bots').selectOption('bot');
+ await wait("document.querySelectorAll('#sessions button').length === 2");
+ await page.locator('#sessions button').first().click();
+ await until(() => streams.has('s1'));
+ emit('node.session.turn.started', { turn_id: 'turn-fixture' });
+ await wait("!document.getElementById('cancel').disabled");
+ await evaluate('probe.controls = 0');
+ const start = performance.now();
+ for (let batch = 0; batch < 250; batch++) {
+ await pause(Math.max(0, start + batch * 20 - performance.now()));
+ for (let i = 0; i < 20; i++) delta(i % 2 ? 'stream-b' : 'stream-a', '文🙂');
+ }
+ await wait("[...document.querySelectorAll('#transcript pre')].filter(n => n.textContent === '文🙂'.repeat(2500)).length === 2");
+ const burst = await evaluate(`(() => {
+ const counts = {};
+ for (const row of probe.writes) counts[row.item + ':' + row.frame] = (counts[row.item + ':' + row.frame] || 0) + 1;
+ const times = [...probe.latencies].sort((a,b) => a-b);
+ return { deltas: times.length, writes: probe.writes.length, maxWritesPerItemFrame: Math.max(...Object.values(counts)), p95Ms: times[Math.ceil(times.length * .95)-1], controls: probe.controls, formattedFolded: probe.formatted, text: [...document.querySelectorAll('#transcript pre')].map(n => n.textContent) };
+ })()`);
+ await writeFile(join(evidence, 'dom-burst.json'), JSON.stringify(burst, null, 2));
+ assert.equal(burst.deltas, 5000); assert.equal(burst.maxWritesPerItemFrame, 1);
+ assert.equal(burst.controls, 0); assert.equal(burst.formattedFolded, 0);
+ assert.ok(burst.p95Ms <= 100, `DOM p95 ${burst.p95Ms} exceeds 100ms`);
+ assert.ok(burst.writes < 1000); burst.elapsedMs = performance.now() - start;
+ await writeFile(join(evidence, 'dom-burst.json'), JSON.stringify(burst, null, 2));
+ await writeFile(join(evidence, 'dom-burst.png'), await page.screenshot({ fullPage: true }));
+ observations.push({ scenario: '1000 deltas/s for 5 seconds, two items, actual textContent setter spy', passed: true });
+
+ await evaluate('probe.holdFrames = true; probe.holdTimers = true');
+ const fragmentId = delta('terminal', '分片 UTF-8 🙂', 's1', true);
+ await wait(`probe.parsed.includes('${fragmentId}')`);
+ assert.equal(await evaluate("document.getElementById('transcript').textContent.includes('分片 UTF-8')"), false);
+ const terminalId = emit('node.session.turn.cancelled', { turn_id: 'turn-fixture' });
+ await wait("document.getElementById('cancel').disabled && document.getElementById('turn-status').textContent.includes('已取消') && document.getElementById('transcript').textContent.includes('分片 UTF-8 🙂')");
+ observations.push({ scenario: 'byte-fragmented UTF-8 and terminal flush while both paint callbacks suspended', passed: true, cursor: terminalId });
+ delta('visibility', 'visible after transition'); await wait("probe.arrivals.visibility?.length === 1");
+ await evaluate("document.dispatchEvent(new Event('visibilitychange'))");
+ assert.equal(await evaluate("document.getElementById('transcript').textContent.includes('visible after transition')"), true);
+ await evaluate("probe.holdTimers = false; Object.defineProperty(document, 'hidden', { configurable: true, value: true })");
+ delta('background', 'timer painted');
+ await wait("document.getElementById('transcript').textContent.includes('timer painted')");
+ observations.push({ scenario: 'visibilitychange flush and timer fallback with rAF suspended', passed: true });
+ await evaluate("delete document.hidden; probe.holdFrames = false; document.getElementById('logs').closest('details').open = true");
+ await wait("document.getElementById('logs').textContent.includes('timer painted')");
+ assert.equal(await evaluate("document.getElementById('logs').textContent.length <= 100000"), true);
+ assert.equal(await evaluate('probe.formatted'), 100);
+ await evaluate("document.getElementById('logs').closest('details').open = false");
+ observations.push({ scenario: 'folded logs format zero events; opening formats only retained 100, display bounded', passed: true });
+
+ hold.items = true; hold.actions = true;
+ emit('node.session.items.changed', {}); emit('node.session.actions.changed', {});
+ await until(() => pending.items.length === 1 && pending.actions.length === 1);
+ emit('node.session.items.changed', {}); emit('node.session.actions.changed', {});
+ await wait(`probe.parsed.includes('evt_${sequence}')`);
+ assert.equal(pending.items.length, 1); assert.equal(pending.actions.length, 1);
+ pending.items[0]([{ id: 'stale', text: 'STALE SNAPSHOT' }]); pending.actions[0]([{ id: 'stale-action' }]);
+ await until(() => pending.items.length === 2 && pending.actions.length === 2);
+ assert.equal(await evaluate("document.body.textContent.includes('STALE SNAPSHOT') || document.body.textContent.includes('stale-action')"), false);
+ itemRows = [{ id: 'latest', text: 'FINAL SNAPSHOT' }]; actionRows = [{ id: 'latest-action', type: 'command' }];
+ pending.items[1](itemRows); pending.actions[1](actionRows); hold.items = false; hold.actions = false;
+ await wait("document.getElementById('transcript').textContent.includes('FINAL SNAPSHOT') && document.getElementById('approvals').textContent.includes('latest-action')");
+ observations.push({ scenario: 'items/actions each one active request plus trailing dirty request, stale snapshot suppressed', passed: true, items: pending.items.length, actions: pending.actions.length });
+
+ await evaluate('probe.holdFrames = true; probe.holdTimers = true');
+ const retainedCursor = delta('stale-paint', 'OLD SESSION MUST NOT PAINT');
+ await wait(`probe.parsed.includes('${retainedCursor}')`);
+ await page.locator('#sessions button').nth(1).click(); await until(() => streams.has('s2'));
+ await evaluate('probe.held.splice(0).forEach(fn => fn()); probe.holdFrames = false; probe.holdTimers = false');
+ assert.equal(await evaluate("document.body.textContent.includes('OLD SESSION MUST NOT PAINT')"), false);
+ await writeFile(join(evidence, 'session-switch.png'), await page.screenshot({ fullPage: true }));
+ await page.locator('#sessions button').first().click(); await until(() => streams.has('s1'));
+ assert.equal(requests.filter(row => row.path === '/v1/agents/sessions/s1/events').at(-1).cursor, retainedCursor);
+ await evaluate('probe.holdFrames = true; probe.holdTimers = true');
+ const endCursor = delta('disconnect', 'flush on EOF');
+ streams.get('s1').end(); await wait("document.getElementById('stream-status').textContent.includes('已断开')");
+ assert.equal(await evaluate("document.getElementById('transcript').textContent.includes('flush on EOF')"), true);
+ await page.locator('#resume').click(); await until(() => streams.has('s1'));
+ assert.equal(requests.filter(row => row.path === '/v1/agents/sessions/s1/events').at(-1).cursor, endCursor);
+ assert.ok(requests.every(row => row.method === 'GET' && row.bearer));
+ observations.push({ scenario: 'session reset cancels captured callbacks, cursors survive switch and EOF, no input replay', passed: true, retainedCursor, endCursor });
+
+ const races = [];
+ await evaluate('probe.holdFrames = false; probe.holdTimers = false');
+ for (const snapshot of ['empty', 'old', 'item-started', 'terminal', 'completed']) {
+ const itemId = 'race-' + snapshot, prefix = snapshot + ' prefix · ', suffix = 'last delta 🙂';
+ const itemType = snapshot === 'item-started' ? 'commandExecution' : 'agentMessage';
+ emit('node.harness.event', { method: 'item/started', params: { turnId: 'race-turn', item: { id: itemId, type: itemType, text: '', status: 'inProgress' } } });
+ delta(itemId, prefix);
+ await wait(`document.getElementById('transcript').textContent.includes(${JSON.stringify(prefix)})`);
+ const barrier = pending.items.length, responses = await evaluate('probe.itemsResponses');
+ hold.items = true; emit('node.session.items.changed', {});
+ await until(() => pending.items.length === barrier + 1);
+ await evaluate('probe.holdFrames = true; probe.holdTimers = true');
+ const id = delta(itemId, suffix);
+ await wait(`probe.parsed.includes('${id}')`);
+ let expected = prefix + suffix;
+ if (snapshot === 'terminal') {
+ emit('node.session.turn.cancelled', { turn_id: 'race-turn' });
+ await wait("document.getElementById('turn-status').textContent.includes('已取消')");
+ }
+ if (snapshot === 'completed') {
+ expected = 'authoritative complete text';
+ const completed = emit('node.harness.event', { method: 'item/completed', params: { turnId: 'race-turn', item: { id: itemId, type: 'agentMessage', text: expected } } });
+ await wait(`probe.parsed.includes('${completed}')`);
+ }
+ const rows = snapshot === 'empty' || snapshot === 'terminal' ? [] : [{ id: itemId, type: itemType, text: 'OLD SNAPSHOT', status: 'inProgress' }];
+ pending.items[barrier](rows); hold.items = false;
+ await wait(`probe.itemsResponses === ${responses + 1}`);
+ await evaluate('probe.held.splice(0).forEach(fn => fn()); probe.holdFrames = false; probe.holdTimers = false');
+ const sample = await evaluate(`({ text: [...document.querySelectorAll('#transcript pre')].map(n => n.textContent), status: document.getElementById('turn-status').textContent, placeholder: document.getElementById('transcript').textContent.includes('没有返回对话记录') })`);
+ races.push({ snapshot, expected, observed: sample });
+ await writeFile(join(evidence, 'refresh-race.json'), JSON.stringify(races, null, 2));
+ assert.equal(sample.text.filter(value => value === expected).length, 1, `${snapshot} snapshot lost or duplicated live text`);
+ assert.equal(sample.text.includes('OLD SNAPSHOT'), false);
+ assert.equal(sample.placeholder, false);
+ }
+ await writeFile(join(evidence, 'refresh-race.png'), await page.screenshot({ fullPage: true }));
+
+ const barrier = pending.items.length;
+ hold.items = true; emit('node.session.items.changed', {});
+ await until(() => pending.items.length === barrier + 1);
+ await evaluate('probe.holdFrames = true; probe.holdTimers = true');
+ const switched = delta('reset-race', 'old session race text');
+ await wait(`probe.parsed.includes('${switched}')`);
+ await page.locator('#sessions button').nth(1).click();
+ await until(() => pending.items.length === barrier + 2);
+ pending.items[barrier]([{ id: 'old-session', text: 'old session race snapshot' }]);
+ pending.items[barrier + 1]([{ id: 'new-session', text: 'NEW SESSION SNAPSHOT' }]); hold.items = false;
+ await until(() => streams.has('s2'));
+ await evaluate('probe.held.splice(0).forEach(fn => fn()); probe.holdFrames = false; probe.holdTimers = false');
+ assert.equal(await evaluate("document.getElementById('transcript').textContent.includes('NEW SESSION SNAPSHOT') && !document.getElementById('transcript').textContent.includes('old session race')"), true);
+ races.push({ snapshot: 'session-reset', passed: true });
+ await writeFile(join(evidence, 'refresh-race.json'), JSON.stringify(races, null, 2));
+ await writeFile(join(evidence, 'refresh-session-switch.png'), await page.screenshot({ fullPage: true }));
+ await writeFile(join(evidence, 'rendering-observations.json'), JSON.stringify(observations, null, 2));
+ await writeFile(join(evidence, 'rendering-requests.json'), JSON.stringify(requests, null, 2));
+ } finally {
+ for (const stream of streams.values()) stream.end();
+ if (browser) await browser.close();
+ server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
+ await rm(profile, { recursive: true, force: true });
+ }
+});
diff --git a/tests/node-agent-viewer-assets.test.mjs b/tests/node-agent-viewer-assets.test.mjs
index 681d236..c6af935 100644
--- a/tests/node-agent-viewer-assets.test.mjs
+++ b/tests/node-agent-viewer-assets.test.mjs
@@ -1,29 +1,69 @@
import test from 'node:test';
import assert from 'node:assert/strict';
-import { mkdtemp, realpath } from 'node:fs/promises';
+import { mkdtemp, realpath, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createSharedRuntime } from '../tools/node-agent-api/shared-runtime.mjs';
test('viewer assets reuse bytes within the container generation without querying bot desktop per module', async () => {
const stateRoot = await realpath(await mkdtemp(join(tmpdir(), 'viewer-assets-')));
- let generation = 'container-a', reads = 0, failed = true;
+ let generation = 'container-a', started = 'start-1', image = 'sha256:img1', reads = 0, inspections = 0, failed = true;
const runtime = await createSharedRuntime({ stateRoot, execute: async (file, args) => {
- if (args[0] === 'inspect') return { stdout: JSON.stringify([{ Id: generation, Name: '/grok-node-local-vm', State: { Running: true } }]) };
+ if (args[0] === 'inspect') { inspections++; return { stdout: JSON.stringify([{ Id: generation, Image: image, Name: '/grok-node-local-vm', State: { Running: true, StartedAt: started } }]) }; }
assert.deepEqual(args.slice(0, 3), ['exec', generation, 'cat']);
reads++;
if (args[3].endsWith('missing.js') && failed) { failed = false; throw new Error('fixture missing'); }
- return { stdout: Buffer.from(generation + ':asset') };
+ return { stdout: Buffer.from(generation + ':' + started + ':' + image) };
} });
try {
const bytes = await Promise.all([runtime.viewerAsset('app/ui.js'), runtime.viewerAsset('app/ui.js')]);
- assert.equal(reads, 1); assert.equal(bytes[0].toString(), 'container-a:asset');
+ assert.equal(reads, 1); assert.equal(bytes[0].toString(), 'container-a:start-1:sha256:img1');
+ assert.equal(inspections, 1);
await runtime.viewerAsset('app/ui.js'); assert.equal(reads, 1);
generation = 'container-b';
- assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'container-b:asset'); assert.equal(reads, 2);
+ assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'container-b:start-1:sha256:img1'); assert.equal(reads, 2);
+ started = 'start-2';
+ assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'container-b:start-2:sha256:img1'); assert.equal(reads, 3);
+ image = 'sha256:img2';
+ assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'container-b:start-2:sha256:img2'); assert.equal(reads, 4);
await assert.rejects(runtime.viewerAsset('core/missing.js'));
- await runtime.viewerAsset('core/missing.js'); assert.equal(reads, 4);
+ await runtime.viewerAsset('core/missing.js'); assert.equal(reads, 6);
await assert.rejects(runtime.viewerAsset('core/../../secret.js'), error => error.status === 404);
- assert.equal(reads, 4);
- } finally { await runtime.close(); }
+ assert.equal(reads, 6);
+ } finally { await runtime.close(); await rm(stateRoot, { recursive: true, force: true }); }
+});
+
+test('asset cache evicts by entries and bytes and never serves stopped containers', async () => {
+ const stateRoot = await realpath(await mkdtemp(join(tmpdir(), 'viewer-bounds-')));
+ let running = true, reads = 0, large = false;
+ const runtime = await createSharedRuntime({ stateRoot, execute: async (_file, args) => {
+ if (args[0] === 'inspect') return { stdout: JSON.stringify([{ Id: 'fixture', Image: 'sha256:img1', Name: '/grok-node-local-vm', State: { Running: running, StartedAt: 'start' } }]) };
+ reads++; return { stdout: Buffer.alloc(large ? 4 * 1024 * 1024 : 10, 65) };
+ } });
+ try {
+ for (let i = 0; i < 130; i++) await runtime.viewerAsset('core/file' + i + '.js');
+ const beforeEntries = reads; await runtime.viewerAsset('core/file0.js'); assert.equal(reads, beforeEntries + 1);
+ large = true;
+ for (let i = 0; i < 6; i++) await runtime.viewerAsset('vendor/large' + i + '.js');
+ const beforeBytes = reads; await runtime.viewerAsset('vendor/large0.js'); assert.equal(reads, beforeBytes + 1);
+ running = false; await assert.rejects(runtime.viewerAsset('vendor/large0.js'), { code: 'execution_unavailable' });
+ assert.equal(reads, beforeBytes + 1);
+ } finally { await runtime.close(); await rm(stateRoot, { recursive: true, force: true }); }
+});
+
+test('pending old-generation bytes cannot poison a replacement cache', async () => {
+ const stateRoot = await realpath(await mkdtemp(join(tmpdir(), 'viewer-pending-')));
+ let generation = 'old', release, entered;
+ const started = new Promise(resolve => { entered = resolve; });
+ const runtime = await createSharedRuntime({ stateRoot, execute: async (_file, args) => {
+ if (args[0] === 'inspect') return { stdout: JSON.stringify([{ Id: generation, Image: 'sha256:img1', Name: '/grok-node-local-vm', State: { Running: true, StartedAt: 'start' } }]) };
+ if (args[1] === 'old') { entered(); await new Promise(resolve => { release = resolve; }); }
+ return { stdout: Buffer.from(args[1]) };
+ } });
+ try {
+ const old = runtime.viewerAsset('app/ui.js'); await started;
+ generation = 'new'; assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'new');
+ release(); await old;
+ assert.equal((await runtime.viewerAsset('app/ui.js')).toString(), 'new');
+ } finally { release?.(); await runtime.close(); await rm(stateRoot, { recursive: true, force: true }); }
});
diff --git a/tests/node-agent-viewer-browser.test.mjs b/tests/node-agent-viewer-browser.test.mjs
new file mode 100644
index 0000000..bbacf55
--- /dev/null
+++ b/tests/node-agent-viewer-browser.test.mjs
@@ -0,0 +1,76 @@
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
+import path from 'node:path';
+import test from 'node:test';
+import { failureKind, kinds, summarizeSamples, verifyPixel, verifyReuse } from '../tools/node-agent-api/bench/viewer-phase-one.mjs';
+import { pattern } from '../tools/node-agent-api/bench/rfb.mjs';
+
+test('browser oracle rejects blank/incorrect pixels and wrong framebuffer dimensions', () => {
+ const frame = { width: pattern.width, height: pattern.height, pixel: [...pattern.rgba] };
+ verifyPixel(frame);
+ for (const changed of [{ pixel: [0, 0, 0, 255] }, { pixel: [37, 149, 211, 0] }, { width: 0 }, { height: 1 }]) {
+ assert.throws(() => verifyPixel({ ...frame, ...changed }), assert.AssertionError);
+ }
+});
+
+test('View reuse oracle independently rejects replacement, authorization and socket churn', () => {
+ const before = { frameId: 1, authorizations: 1, connections: 1 };
+ verifyReuse(before, { ...before });
+ for (const key of Object.keys(before)) assert.throws(() => verifyReuse(before, { ...before, [key]: 2 }), assert.AssertionError);
+});
+
+test('latency quantiles keep unprewarmed, prewarmed and repeat samples separate', () => {
+ const rows = [40, 10, 30, 20].map(firstPixelMs => ({ kind: kinds[0], firstPixelMs }));
+ rows.push({ kind: kinds[1], firstPixelMs: 2 });
+ assert.deepEqual(summarizeSamples(rows), {
+ 'no-prewarm-first-open': { samples: 4, p50: 20, p95: 40 },
+ 'prewarmed-open': { samples: 1, p50: 2, p95: 2 },
+ 'repeat-open': { samples: 0, p50: null, p95: null },
+ });
+});
+
+test('browser runtime failures never turn product assertions into transient passes', () => {
+ assert.equal(failureKind({ name: 'NavigationReadinessTimeoutError', message: 'navigation' }), 'browser-runtime');
+ assert.equal(failureKind(new Error('Product condition timeout: hidden prewarm first-frame')), 'product-or-contract');
+ assert.equal(failureKind(new assert.AssertionError({ message: 'extra authorization' })), 'product-or-contract');
+});
+
+// Opt into the installed browser, without downloading dependencies:
+// NODE_AGENT_VIEWER_BROWSER_LOADER=/installed/omowright.mjs node --test tests/node-agent-viewer-browser.test.mjs
+// The CLI is also independently runnable with --loader, --output and --samples.
+if (process.env.NODE_AGENT_VIEWER_BROWSER_LOADER) {
+ test('controlled Chromium exercises real API, fake Codex, product UI and bundled noVNC', { timeout: 240000 }, async () => {
+ const root = path.resolve(import.meta.dirname, '..');
+ const output = path.resolve(process.env.NODE_AGENT_VIEWER_BROWSER_OUTPUT ?? path.join(root, '.omo/evidence/viewer-phase-one-sidecar/browser-test.json'));
+ await mkdir(path.dirname(output), { recursive: true });
+ const args = [path.join(root, 'tools/node-agent-api/bench/viewer-phase-one.mjs'), '--loader', process.env.NODE_AGENT_VIEWER_BROWSER_LOADER, '--output', output, '--samples', '1'];
+ let log = '';
+ const code = await new Promise((resolve, reject) => {
+ const child = spawn(process.execPath, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
+ child.stdout.on('data', data => { log += data; });
+ child.stderr.on('data', data => { log += data; });
+ child.once('error', reject); child.once('exit', resolve);
+ });
+ await writeFile(output + '.log', log, { mode: 0o600 });
+ const report = JSON.parse(await readFile(output, 'utf8'));
+ assert.equal(code, 0, JSON.stringify(report.failures));
+ assert.equal(report.pass, true);
+ assert.equal(report.sourceStable, true);
+ assert.deepEqual(report.failures, []);
+ for (const kind of kinds) assert.equal(report.summary[kind].samples, 1);
+ const [group] = report.groups;
+ assert.equal(group.checks.length, 7);
+ assert.ok(group.checks.every(check => check.pass));
+ assert.equal(group.cleanup.browserClosed, true);
+ assert.equal(group.cleanup.profileRemoved, true);
+ assert.equal(group.cleanup.fixture.children, 0);
+ assert.equal(group.cleanup.fixture.stateRemoved, true);
+ assert.deepEqual(group.cleanup.fixture.credentialAudit.violations, []);
+ assert.deepEqual(group.cleanup.rfb, { sockets: 0, timers: 0 });
+ assert.equal(group.fixtureCounters.realDockerCalls, 0);
+ assert.equal(group.fixtureCounters.turnStart, 0);
+ assert.ok(group.artifacts.length >= 2);
+ for (const artifact of group.artifacts) assert.ok((await stat(artifact)).size > 0);
+ });
+}
diff --git a/tests/node-agent-viewer-lanes.test.mjs b/tests/node-agent-viewer-lanes.test.mjs
new file mode 100644
index 0000000..5b43ac6
--- /dev/null
+++ b/tests/node-agent-viewer-lanes.test.mjs
@@ -0,0 +1,166 @@
+import assert from 'node:assert/strict';
+import { once } from 'node:events';
+import { createServer } from 'node:http';
+import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import test from 'node:test';
+import { WebSocket, WebSocketServer } from 'ws';
+import { startNodeAgentApi } from '../tools/node-agent-api/server.mjs';
+import { ApiError } from '../tools/node-agent-api/errors.mjs';
+
+async function fixture(t, { embeddedViewer = null, validate = true } = {}) {
+ const directory = await realpath(await mkdtemp(path.join(tmpdir(), 'viewer-lanes-')));
+ const http = createServer(), upstream = new WebSocketServer({ server: http });
+ const state = { assigned: true, generation: 'a', prepare: [], validated: 0, received: [] };
+ upstream.on('connection', socket => {
+ socket.send('RFB 003.008\n');
+ socket.on('message', data => { state.received.push(Buffer.from(data)); socket.send(data); });
+ });
+ await new Promise(resolve => http.listen(0, '127.0.0.1', resolve));
+ const bot = { id: 'bot-a', backend: 'grok_node' };
+ const adapter = {
+ async agents() { return [bot]; }, async requireAgent() { return bot; }, async items() { return []; },
+ async desktop(id, target, mode, options) {
+ state.prepare.push(options.prepare);
+ if (!state.assigned && !options.prepare) throw new ApiError(409, 'desktop_unavailable', 'Unassigned');
+ state.assigned = true;
+ return { display: 3, applicationId: target.application_id ?? null, websocketUrl: `ws://127.0.0.1:${http.address().port}`, containerGenerationImmutableId: state.generation };
+ },
+ async viewerAsset() { return 'fallback'; },
+ ...(validate ? { async validateDesktop(id, expected) {
+ state.validated++;
+ assert.equal(id, 'bot-a');
+ if (state.hang) return new Promise(() => {});
+ if (!state.assigned || expected.containerGenerationImmutableId !== state.generation) throw new ApiError(409, 'desktop_changed', 'Changed');
+ return true;
+ } } : {}),
+ };
+ const service = await startNodeAgentApi({ adapter, stateDirectory: directory, embeddedViewer });
+ const authorization = 'Bearer ' + (await readFile(service.keyFile, 'utf8')).trim();
+ const request = (route, { body, headers = {}, method = body ? 'POST' : 'GET' } = {}) => fetch(service.origin + route, {
+ method, headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...headers },
+ ...(body ? { body: JSON.stringify(body) } : {}), redirect: 'manual', signal: AbortSignal.timeout(9000),
+ });
+ const created = await request('/v1/agents/sessions', { body: { agent_id: bot.id }, headers: { authorization } });
+ assert.equal(created.status, 201);
+ const session = await created.json(), route = `/v1/agents/sessions/${session.id}/desktop`;
+ const issue = (body = {}, auth = authorization) => request(route, { body, headers: { authorization: auth } });
+ async function open(body = {}, auth) {
+ const response = await issue(body, auth); assert.equal(response.status, 201);
+ const ticket = new URL((await response.json()).url);
+ const opened = await request(ticket.pathname + ticket.search); assert.equal(opened.status, 303);
+ const location = opened.headers.get('location');
+ assert.match(location, /^\/desktop\/lane\/[A-Za-z0-9_-]+\/vnc.html$/);
+ const base = location.slice(0, -'vnc.html'.length), cookie = opened.headers.get('set-cookie').split(';')[0];
+ assert.ok(opened.headers.get('set-cookie').includes('Path=' + base.slice(0, -1) + ';'));
+ return { base, cookie, location };
+ }
+ async function connect(viewer) {
+ const socket = new WebSocket(service.origin.replace('http:', 'ws:') + viewer.base + 'socket', { headers: { cookie: viewer.cookie, origin: service.origin } });
+ socket.on('error', () => {});
+ const banner = once(socket, 'message');
+ await once(socket, 'open'); assert.equal((await banner)[0].toString(), 'RFB 003.008\n');
+ return socket;
+ }
+ async function close(viewer, headers = { cookie: viewer.cookie, origin: service.origin }) {
+ return request(viewer.base + 'close', { method: 'POST', headers });
+ }
+ t.after(async () => {
+ await service.close();
+ for (const socket of upstream.clients) socket.terminate();
+ await new Promise(resolve => upstream.close(resolve));
+ await new Promise(resolve => http.close(resolve));
+ await rm(directory, { recursive: true, force: true });
+ });
+ return { state, request, issue, open, connect, close, service, authorization, session };
+}
+
+test('existing-only is view-only, never prepares missing desktops, and validates its body', async t => {
+ const f = await fixture(t);
+ f.state.assigned = false;
+ assert.equal((await f.issue({ existing_only: true })).status, 409);
+ assert.deepEqual(f.state.prepare, [false]); assert.equal(f.state.assigned, false);
+ for (const body of [{ existing_only: 'true' }, { mode: 'control', existing_only: true }]) assert.equal((await f.issue(body)).status, 400);
+ assert.deepEqual(f.state.prepare, [false]);
+ f.state.assigned = true;
+ const viewer = await f.open({ mode: 'view', existing_only: true, replace_own_control: false });
+ await f.connect(viewer);
+ assert.deepEqual(f.state.prepare, [false, false, false, false]);
+});
+
+test('desktop and application viewers have isolated cookies, sockets and Cookie+Origin disposal', async t => {
+ const f = await fixture(t);
+ const a = await f.open(), b = await f.open(), app = await f.open({ target: { type: 'application', application_id: '0x200004' } });
+ assert.equal(new Set([a.base, b.base, app.base]).size, 3);
+ const sockets = await Promise.all([a, b, app].map(f.connect));
+ for (const headers of [{ cookie: a.cookie }, { cookie: a.cookie, origin: 'https://foreign.invalid' }, { origin: f.service.origin }]) {
+ assert.ok([401, 403].includes((await f.close(a, headers)).status));
+ }
+ assert.equal((await f.request(b.location, { headers: { cookie: a.cookie } })).status, 401);
+ assert.equal((await f.request('/desktop/vnc.html', { headers: { cookie: a.cookie } })).status, 401);
+ assert.equal((await f.close(b, { cookie: a.cookie, origin: f.service.origin })).status, 401);
+ const closed = once(sockets[0], 'close');
+ assert.equal((await f.close(a)).status, 200); await closed;
+ assert.equal((await f.request(a.location, { headers: { cookie: a.cookie } })).status, 401);
+ for (let i = 1; i < sockets.length; i++) {
+ const socket = sockets[i], response = once(socket, 'message'); socket.send('RFB 003.008\n');
+ assert.equal((await response)[0].toString(), 'RFB 003.008\n');
+ }
+ assert.equal((await f.request(b.location, { headers: { cookie: a.cookie + '; ' + b.cookie } })).status, 200);
+});
+
+test('closing a view lane preserves the control lease; closing control releases it', async t => {
+ const f = await fixture(t), controller = await f.open({ mode: 'control' }), viewer = await f.open();
+ await f.connect(controller); await f.connect(viewer);
+ assert.equal((await f.close(viewer)).status, 200);
+ assert.equal((await f.issue({ mode: 'control' })).status, 409);
+ assert.equal((await f.close(controller)).status, 200);
+ assert.equal((await f.issue({ mode: 'control' })).status, 201);
+});
+
+for (const change of ['generation', 'assignment', 'timeout']) test('retained sockets fail closed on ' + change, { timeout: 12000 }, async t => {
+ const f = await fixture(t), viewer = await f.open(), socket = await f.connect(viewer);
+ const closed = once(socket, 'close', { signal: AbortSignal.timeout(8500) });
+ if (change === 'generation') f.state.generation = 'b';
+ if (change === 'assignment') f.state.assigned = false;
+ if (change === 'timeout') f.state.hang = true;
+ await closed; assert.ok(f.state.validated >= 1);
+ assert.equal(socket.readyState, WebSocket.CLOSED);
+ assert.ok(f.state.prepare.slice(1).every(value => value === false));
+});
+
+for (const action of ['expiry', 'revoke', 'session-close', 'handback']) test('retained connection terminates on ' + action, { timeout: 7000 }, async t => {
+ const f = await fixture(t, { validate: false });
+ const keyResponse = await f.request('/v1/keys', { body: { scopes: ['sessions.read', 'desktop.view', 'desktop.control'], bot_ids: ['bot-a'], ttl_seconds: action === 'expiry' ? 2 : 60 }, headers: { authorization: f.authorization } });
+ assert.equal(keyResponse.status, 201);
+ const key = await keyResponse.json();
+ const viewer = await f.open({ mode: action === 'handback' ? 'control' : 'view' }, 'Bearer ' + key.key);
+ const socket = await f.connect(viewer), closed = once(socket, 'close', { signal: AbortSignal.timeout(5000) });
+ if (action === 'revoke') assert.equal((await f.request('/v1/keys/' + key.id, { method: 'DELETE', headers: { authorization: f.authorization } })).status, 200);
+ if (action === 'session-close' || action === 'handback') assert.equal((await f.request(`/v1/agents/sessions/${f.session.id}/${action === 'handback' ? 'handback' : 'close'}`, { method: 'POST', headers: { authorization: f.authorization } })).status, 200);
+ await closed;
+ assert.equal((await f.request(viewer.location, { headers: { cookie: viewer.cookie } })).status, 401);
+});
+
+test('injected embedded HTML and bundle are lane protected and fallback remains available', async t => {
+ let args;
+ const bundle = 'viewer-' + 'a'.repeat(64) + '.js';
+ const f = await fixture(t, { embeddedViewer: {
+ html(input) { args = input; return ``; },
+ asset(asset) { return asset === bundle ? { body: 'export const ready = true;', contentType: 'text/javascript', cacheControl: 'private, max-age=60' } : null; },
+ } });
+ const viewer = await f.open();
+ const html = await f.request(viewer.location, { headers: { cookie: viewer.cookie } });
+ assert.equal(html.status, 200); assert.match(await html.text(), new RegExp(bundle.replace('.', '\\.')));
+ assert.equal((await f.request(viewer.location, { headers: { cookie: viewer.cookie, 'if-none-match': '*' } })).status, 200);
+ assert.equal(args.mode, 'view'); assert.equal(args.socketPath, viewer.base + 'socket'); assert.ok(args.expiresAt > Date.now());
+ const asset = await f.request(viewer.base + bundle, { headers: { cookie: viewer.cookie } });
+ assert.equal(asset.status, 200); assert.equal(await asset.text(), 'export const ready = true;');
+ assert.equal(asset.headers.get('content-type'), 'text/javascript'); assert.equal(asset.headers.get('cache-control'), 'private, max-age=60');
+ assert.equal((await f.request(viewer.base + bundle)).status, 401);
+ assert.equal((await f.request(viewer.base + 'unknown.js', { headers: { cookie: viewer.cookie } })).status, 404);
+ const legacy = await fixture(t, { validate: false }), oldViewer = await legacy.open();
+ const fallback = await legacy.request(oldViewer.location, { headers: { cookie: oldViewer.cookie } });
+ assert.match(await fallback.text(), /fallback/); await legacy.connect(oldViewer);
+});
diff --git a/tests/node-agent-viewer-lifecycle.test.mjs b/tests/node-agent-viewer-lifecycle.test.mjs
new file mode 100644
index 0000000..fa10545
--- /dev/null
+++ b/tests/node-agent-viewer-lifecycle.test.mjs
@@ -0,0 +1,234 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import vm from 'node:vm';
+import { readFile, mkdir, writeFile } from 'node:fs/promises';
+import * as helpers from '../tools/node-agent-api/web/client.js';
+
+const source = await readFile(new URL('../tools/node-agent-api/web/app.js', import.meta.url), 'utf8');
+const evidence = new URL('../.omo/evidence/vnc-frontend-phase-one/', import.meta.url);
+await mkdir(evidence, { recursive: true });
+const observations = [];
+test.after(async () => writeFile(new URL('scenarios.json', evidence), JSON.stringify(observations, null, 2)));
+
+function fixture() {
+ let now = 100000, nextTimer = 0;
+ const timers = new Map(), elements = new Map(), requests = [], closes = [], messages = [], listeners = new Map(), aliveChecks = [];
+ let alive = async () => ({ ok: true, json: async () => ({ view_only: true }) });
+ class Element {
+ constructor() {
+ this.textContent = ''; this.value = ''; this.children = []; this.disabled = false;
+ this.events = new Map(); this.contentDocument = { body: { textContent: '' } };
+ this.contentWindow = { location: { href: 'http://localhost/desktop/lane/fixture/vnc.html' }, postMessage: (data, origin) => messages.push({ data, origin, iframe: this }) };
+ }
+ addEventListener(type, fn) { this.events.set(type, fn); }
+ removeEventListener(type, fn) { if (this.events.get(type) === fn) this.events.delete(type); }
+ replaceChildren(...nodes) { this.children = nodes; for (const n of nodes) n.parentElement = this; }
+ append(...nodes) { this.children.push(...nodes); for (const n of nodes) n.parentElement = this; }
+ closest() { return new Element(); }
+ querySelectorAll() { return []; }
+ close() {}
+ remove() { if (this.parentElement) this.parentElement.children = this.parentElement.children.filter(n => n !== this); }
+ }
+ const $ = id => { if (!elements.has(id)) elements.set(id, new Element()); return elements.get(id); };
+ const api = {
+ json(path, options) {
+ if (!path.endsWith('/desktop') && !path.endsWith('/handback')) return Promise.resolve({ id: path.split('/').at(-1), agent_id: 'bot', status: 'idle' });
+ return new Promise((resolve, reject) => requests.push({ path, ...options, resolve, reject }));
+ },
+ list: async () => [],
+ };
+ const sandbox = {
+ ...helpers, AbortController, AbortSignal, DOMException, URL, Option: Element, console,
+ Date: { now: () => now }, location: { origin: 'http://localhost' },
+ setTimeout(fn, ms) { timers.set(++nextTimer, { fn, at: now + ms }); return nextTimer; },
+ clearTimeout: id => timers.delete(id),
+ fetch: async (url, options) => {
+ const record = { url: String(url), options };
+ if (String(url).endsWith('/mandatory.json')) { aliveChecks.push(record); return alive(); }
+ closes.push(record); return {};
+ },
+ document: { getElementById: $, createElement: () => new Element(), createDocumentFragment: () => new Element(), addEventListener() {} },
+ window: { addEventListener: (type, fn) => listeners.set(type, fn) },
+ createPaintQueue: () => ({ reset() {}, schedule() {}, flush() {} }),
+ };
+ vm.createContext(Object.assign(sandbox, { api }));
+ vm.runInContext(source.replace(/^import[^\n]+\n/, '') + `\n globalThis.probe = {
+ desktop, resetSession, disconnect, selectSession, receive, scheduleDesktopPrewarm,
+ seed(id = 's1') { client = api; botId = 'bot'; session = {id, status:'idle'}; flags = {view:true, control:true}; },
+ clear() { session = undefined; }, closed() { session.status = 'closed'; },
+ viewer: () => currentViewer, changing: () => desktopChanging,
+ };`, sandbox);
+ const p = sandbox.probe;
+ const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); };
+ return {
+ p, $, requests, timers, messages, closes, flush, aliveChecks,
+ setAlive(fn) { alive = fn; },
+ async tick(ms) { now += ms; for (const [id, timer] of [...timers]) if (timer.at <= now) { timers.delete(id); timer.fn(); } await flush(); },
+ async authorize(request = requests.at(-1)) { request.resolve({ url: '/desktop/open?ticket=fixture', pauses_agent: false }); await flush(); return p.viewer()?.iframe; },
+ async load(frame = p.viewer().iframe) { frame.events.get('load')?.(); await flush(); },
+ signal(type, overrides = {}, event = {}) { const viewer = p.viewer(); listeners.get('message')({ origin: 'http://localhost', source: viewer?.iframe.contentWindow, data: {source:'node-agent-viewer', mode:viewer?.mode, expiresAt: now + 1000, type, ...overrides}, ...event }); },
+ async healthy() { this.signal('ready'); this.signal('connected'); this.signal('first-frame'); await this.load(); },
+ record(name) { observations.push({ scenario: name, observable: 'PASS: all executable assertions satisfied', requests: requests.map(r => ({path:r.path, body:r.body, aborted:r.signal.aborted})), frames: $('desktop').children.length, disposeMessages: messages.length, laneCloses: closes.length }); },
+ };
+}
+
+async function scenario(t, action) {
+ const f = fixture();
+ try { await action(f); f.record(t.name); } finally { f.p.resetSession(); }
+}
+
+test('selection debounce issues only last-session existing_only view, never auth-only or closed-session prewarm', t => scenario(t, async f => {
+ f.p.clear(); f.p.scheduleDesktopPrewarm(); await f.tick(401); assert.equal(f.requests.length, 0);
+ f.p.seed('s1'); f.p.scheduleDesktopPrewarm(); await f.tick(100);
+ f.p.resetSession(); f.p.seed('s2'); f.p.scheduleDesktopPrewarm(); await f.tick(401);
+ assert.equal(f.requests.length, 1); assert.ok(f.requests[0].path.includes('/s2/'));
+ assert.deepEqual(JSON.parse(JSON.stringify(f.requests[0].body)), { mode:'view', replace_own_control:false, existing_only:true });
+ f.p.resetSession(); f.p.seed(); f.p.closed(); f.p.scheduleDesktopPrewarm(); await f.tick(401); assert.equal(f.requests.length, 1);
+}));
+
+test('actual selectSession schedules quiet prewarm; missing desktop falls through explicit prepare', t => scenario(t, async f => {
+ f.p.seed(); await f.p.selectSession('selected'); await f.tick(401);
+ assert.ok(f.requests[0].path.includes('/selected/'));
+ const before = f.$('desktop-status').textContent;
+ f.requests[0].reject(new Error('desktop not running')); await f.flush();
+ assert.equal(f.$('desktop-status').textContent, before); assert.equal(f.p.viewer(), undefined);
+ const open = f.p.desktop('view'); assert.equal(f.requests.length, 2); assert.equal(f.requests[1].body.existing_only, undefined);
+ await f.authorize(); await f.load(); await open;
+}));
+
+test('healthy current view reuses iframe without authorization; ready/load alone never reuses', t => scenario(t, async f => {
+ f.p.seed(); const warm = f.p.desktop('view', false, true); await f.authorize(); f.signal('ready'); await f.load(); await warm;
+ const old = f.p.viewer().iframe; const open = f.p.desktop('view'); assert.equal(f.requests.length, 2);
+ assert.equal(f.messages[0].iframe, old); await f.authorize(); await f.healthy(); await open;
+ const frame = f.p.viewer().iframe; await f.p.desktop('view'); assert.equal(f.requests.length, 2); assert.equal(f.p.viewer().iframe, frame);
+ assert.equal(f.$('desktop').children.length, 1);
+}));
+
+test('prewarmed first frame is revealed on View with no new request; mode is never promoted', t => scenario(t, async f => {
+ f.p.seed(); const warm = f.p.desktop('view', false, true); await f.authorize(); await f.healthy(); await warm;
+ const frame = f.p.viewer().iframe; assert.equal(frame.hidden, true);
+ await f.p.desktop('view'); assert.equal(frame.hidden, false); assert.equal(f.requests.length, 1);
+ const control = f.p.desktop('control'); assert.equal(f.requests.length, 2); assert.equal(f.requests[1].body.mode, 'control'); assert.equal(f.requests[1].body.replace_own_control, true);
+ await f.authorize(); await f.healthy(); await control; assert.notEqual(f.p.viewer().iframe, frame);
+ const view = f.p.desktop('view'); assert.equal(f.requests.length, 3); assert.equal(f.requests[2].body.mode, 'view'); await f.authorize(); await f.load(); await view;
+}));
+
+test('legacy iframe load succeeds but successive View always obtains new authorization', t => scenario(t, async f => {
+ f.p.seed(); const a = f.p.desktop('view'); await f.authorize(); await f.load(); await a;
+ assert.equal(f.p.changing(), false); assert.match(f.$('desktop-status').textContent, /已签发观看授权/);
+ const b = f.p.desktop('view'); assert.equal(f.requests.length, 2); await f.authorize(); await f.load(); await b;
+}));
+
+test('wrong origin/window/source/mode and first-frame before connected cannot grant reuse', t => scenario(t, async f => {
+ f.p.seed(); const a = f.p.desktop('view'); await f.authorize(); await f.load(); await a;
+ f.signal('ready', {}, {origin:'https://evil.invalid'}); assert.equal(f.p.viewer().expiresAt, 0);
+ f.signal('ready', {}, {source:{}}); assert.equal(f.p.viewer().expiresAt, 0);
+ f.signal('ready', {source:'other'}); assert.equal(f.p.viewer().expiresAt, 0);
+ f.signal('ready', {mode:'control'}); assert.equal(f.p.viewer().expiresAt, 0);
+ f.signal('ready'); f.signal('first-frame'); assert.equal(f.p.viewer().firstFrame, false);
+ f.signal('connected'); assert.equal(f.p.viewer().firstFrame, false);
+ const b = f.p.desktop('view'); assert.equal(f.requests.length, 2); await f.authorize(); await f.load(); await b;
+}));
+
+test('expiry disposes lane and frame; disconnected similarly prevents reuse', t => scenario(t, async f => {
+ f.p.seed(); const a = f.p.desktop('view'); await f.authorize(); await f.healthy(); await a;
+ await f.tick(1001); assert.equal(f.p.viewer(), undefined); assert.equal(f.$('desktop').children.length, 0); assert.equal(f.closes.length, 1);
+ assert.equal(f.closes[0].url, 'http://localhost/desktop/lane/fixture/close'); assert.equal(f.closes[0].options.method, 'POST');
+ assert.deepEqual(JSON.parse(JSON.stringify(f.messages[0].data)), {source:'node-agent-parent', type:'dispose'});
+ const b = f.p.desktop('view'); await f.authorize(); await f.healthy(); await b; f.signal('disconnected');
+ assert.equal(f.p.viewer(), undefined); assert.equal(f.closes.length, 2);
+}));
+
+test('selection and disconnect fence delayed authorization, cancel load waits and expiry timers', t => scenario(t, async f => {
+ f.p.seed(); const warm = f.p.desktop('view', false, true); const stale = f.requests[0];
+ f.p.resetSession(); f.p.seed('s2'); await f.authorize(stale); await warm; assert.equal(f.p.viewer(), undefined); assert.equal(stale.signal.aborted, true);
+ const a = f.p.desktop('view'); const cancelled = assert.rejects(a, {name:'AbortError'}); await f.authorize(); f.signal('ready'); f.p.disconnect(); await cancelled;
+ assert.equal(f.p.viewer(), undefined); assert.equal(f.timers.size, 0); assert.equal(f.$('desktop').children.length, 0);
+}));
+
+test('explicit Control cancels in-flight prewarm and stale completion cannot overwrite current viewer', t => scenario(t, async f => {
+ f.p.seed(); const warm = f.p.desktop('view', false, true), stale = f.requests[0];
+ const control = f.p.desktop('control'); await f.authorize(); await f.healthy(); await control;
+ const frame = f.p.viewer().iframe; await f.authorize(stale); await warm;
+ assert.equal(stale.signal.aborted, true); assert.equal(f.p.viewer().iframe, frame); assert.equal(f.p.viewer().mode, 'control');
+}));
+
+test('Handback failure retains control; confirmation precedes fresh view; view failure reports released state', t => scenario(t, async f => {
+ f.p.seed(); const a = f.p.desktop('control'); await f.authorize(); await f.healthy(); await a;
+ const frame = f.p.viewer().iframe;
+ const failed = f.p.desktop('view', true); const rejected = assert.rejects(failed, /denied/); assert.ok(f.requests[1].path.endsWith('/handback'));
+ f.requests[1].reject(new Error('denied')); await rejected; assert.equal(f.p.viewer().iframe, frame);
+ const b = f.p.desktop('view', true); assert.equal(f.requests.length, 3); assert.equal(f.p.viewer().iframe, frame);
+ f.requests[2].resolve({}); await f.flush(); assert.equal(f.requests.length, 4); assert.equal(f.p.viewer(), undefined); assert.equal(f.requests[3].body.mode, 'view');
+ await f.authorize(); await f.healthy(); await b; assert.notEqual(f.p.viewer().iframe, frame);
+ const c = f.p.desktop('view', true); const reconnect = assert.rejects(c, /控制已交还,但观看重连失败/);
+ f.requests[4].resolve({}); await f.flush(); f.requests[5].reject(new Error('offline')); await reconnect;
+}));
+
+test('load error and timeout dispose frame; session closed event also disposes', t => scenario(t, async f => {
+ f.p.seed(); const a = f.p.desktop('view'); const invalid = assert.rejects(a, /桌面授权已变化/); const frame = await f.authorize();
+ frame.contentDocument.body.textContent = '{"error":"expired"}'; await f.load(); await invalid; assert.equal(f.p.viewer(), undefined);
+ const b = f.p.desktop('view'); const timeout = assert.rejects(b, /加载超时/); await f.authorize(); await f.tick(30000); await timeout; assert.equal(f.p.viewer(), undefined);
+ const c = f.p.desktop('view'); await f.authorize(); await f.healthy(); await c;
+ f.p.receive({type:'node.session.closed', data:{session_id:'s1'}}); assert.equal(f.p.viewer(), undefined); assert.equal(f.$('desktop').children.length, 0);
+}));
+
+test('server disconnect before Handback response does not abort confirmed view reconnect', t => scenario(t, async f => {
+ f.p.seed(); const control = f.p.desktop('control'); await f.authorize(); await f.healthy(); await control;
+ const handback = f.p.desktop('view', true), request = f.requests[1];
+ f.signal('disconnected'); assert.equal(f.p.viewer(), undefined); assert.equal(request.signal.aborted, false);
+ request.resolve({}); await f.flush(); assert.equal(f.requests.length, 3); assert.equal(f.requests[2].body.mode, 'view');
+ await f.authorize(); await f.healthy(); await handback; assert.equal(f.p.viewer().mode, 'view');
+}));
+
+test('explicit View cancels loading prewarm; detached iframe messages and late load cannot dispose replacement', t => scenario(t, async f => {
+ f.p.seed(); const warm = f.p.desktop('view', false, true); const old = await f.authorize();
+ const lateLoad = old.events.get('load'); const view = f.p.desktop('view'); await warm;
+ assert.equal(old.events.size, 0); const frame = await f.authorize(); await f.healthy(); await view;
+ f.signal('disconnected', {}, {source:old.contentWindow}); lateLoad(); await f.flush();
+ assert.equal(f.p.viewer().iframe, frame); assert.equal(f.$('desktop').children.length, 1); assert.equal(f.p.changing(), false);
+}));
+
+test('cold View before 400ms cancels scheduled prewarm; injectable delay and disabled scheduling stay internal', t => scenario(t, async f => {
+ f.p.seed(); f.p.scheduleDesktopPrewarm({ enabled: false }); await f.tick(500); assert.equal(f.requests.length, 0);
+ f.p.scheduleDesktopPrewarm(); await f.tick(399); assert.equal(f.requests.length, 0);
+ const cold = f.p.desktop('view'); await f.authorize(); await f.healthy(); await cold; await f.tick(500);
+ assert.equal(f.requests.length, 1); assert.equal(f.requests[0].body.existing_only, undefined);
+ f.p.resetSession(); f.p.seed(); f.p.scheduleDesktopPrewarm({delay:10}); await f.tick(10);
+ assert.equal(f.requests.length, 2); assert.equal(f.requests[1].body.existing_only, true);
+}));
+
+test('View joins started scheduled prewarm only when healthy; legacy joined load falls through fresh prepare', t => scenario(t, async f => {
+ f.p.seed(); f.p.scheduleDesktopPrewarm(); await f.tick(400);
+ const joined = f.p.desktop('view'); assert.equal(f.requests.length, 1); assert.equal(f.p.changing(), true);
+ await f.authorize(); await f.healthy(); await joined;
+ assert.equal(f.requests.length, 1); assert.equal(f.p.viewer().iframe.hidden, false);
+ f.p.resetSession(); f.p.seed(); f.p.scheduleDesktopPrewarm(); await f.tick(400);
+ const legacy = f.p.desktop('view'); await f.authorize(); await f.load();
+ assert.equal(f.requests.length, 3); assert.equal(f.requests[2].body.existing_only, undefined);
+ await f.authorize(); await f.load(); await legacy;
+}));
+
+test('inactivity hides old canvas until uncached lane alive check; revoked lane requires fresh authorization', t => scenario(t, async f => {
+ f.p.seed(); const open = f.p.desktop('view'); await f.authorize();
+ f.signal('ready', {expiresAt:300000}); f.signal('connected'); f.signal('first-frame'); await f.load(); await open;
+ const frame = f.p.viewer().iframe;
+ let resolve; f.setAlive(() => new Promise(done => { resolve = done; })); await f.tick(30001);
+ const reuse = f.p.desktop('view'); assert.equal(frame.hidden, true); assert.equal(f.requests.length, 1);
+ assert.equal(f.aliveChecks[0].options.cache, 'no-store'); assert.equal(f.aliveChecks[0].options.credentials, 'same-origin');
+ resolve({ok:true, json:async () => ({view_only:true})}); await reuse;
+ assert.equal(frame.hidden, false); assert.equal(f.p.viewer().iframe, frame);
+ await f.tick(30001); f.setAlive(async () => ({ok:false}));
+ const renewed = f.p.desktop('view'); await f.flush(); assert.equal(f.p.viewer(), undefined); assert.equal(f.requests.length, 2);
+ await f.authorize(); await f.healthy(); await renewed; assert.notEqual(f.p.viewer().iframe, frame);
+}));
+
+test('selection during joined prewarm or lane alive check cannot reveal old canvas or open new session implicitly', t => scenario(t, async f => {
+ f.p.seed(); f.p.scheduleDesktopPrewarm(); await f.tick(400); const stale = f.requests[0];
+ const joined = f.p.desktop('view'); f.p.resetSession(); f.p.seed('s2'); await f.authorize(stale); await joined;
+ assert.equal(f.p.viewer(), undefined); assert.equal(f.requests.length, 1);
+ const open = f.p.desktop('view'); await f.authorize(); f.signal('ready', {expiresAt:300000}); f.signal('connected'); f.signal('first-frame'); await f.load(); await open;
+ let resolve; f.setAlive(() => new Promise(done => {resolve = done;})); await f.tick(30001);
+ const reuse = f.p.desktop('view'); f.p.resetSession(); f.p.seed('s3'); resolve({ok:true,json:async () => ({view_only:true})}); await reuse;
+ assert.equal(f.p.viewer(), undefined); assert.equal(f.requests.length, 2); assert.equal(f.aliveChecks[0].options.signal.aborted, true);
+}));
diff --git a/tools/node-agent-api/README.md b/tools/node-agent-api/README.md
index 96c4d42..0c00ebf 100644
--- a/tools/node-agent-api/README.md
+++ b/tools/node-agent-api/README.md
@@ -77,7 +77,18 @@ desktop 返回短期授权链接;默认观看。--mode control 明确申请接
| desktop.control | 接管、交还、手动写剪贴板和打开应用 |
| keys.manage | 管理授权范围内的派生 key |
-桌面 ticket 默认 60 秒、最多 300 秒,单次兑换为 HttpOnly、SameSite=Strict cookie,viewer 有效期 15 分钟。应用窗口各自使用 cookie 路径,可以同时观看多个应用;每 bot 只有一个控制租约。观看输入在服务端过滤,交还、会话关闭或撤销 key 断开对应授权。
+桌面 ticket 默认 60 秒、最多 300 秒,单次兑换为 HttpOnly、SameSite=Strict cookie,viewer 有效期 15 分钟。完整桌面和应用窗口均使用各自的 `/desktop/lane//` 路径及 cookie;每 bot 只有一个控制租约。观看输入在服务端过滤,交还、会话关闭或撤销 key 断开对应授权。
+
+网页只保留当前会话的一个查看器。会话环境已可用时,后台请求 `mode=view, existing_only=true, replace_own_control=false`,只连接已有桌面;点击观看可复用已完成真实首帧且未过期的连接。接管和交还仍重新取得或释放服务器租约,切换会话会销毁旧查看器。共享容器的只读状态通道每次重新读取桌面分配和 gateway 状态,并检查容器 ID/StartedAt;保留连接周期校验归属,异常时关闭。
+
+内嵌查看器使用构建时打包的 noVNC 核心,画质和压缩等级沿用原值。构建使用已安装的 esbuild;运行 API 不加载 esbuild。将当前容器的 `/usr/share/novnc` 只读复制到本机临时目录后运行:
+
+```sh
+export PATH="/Users/Apple/Documents/grokbot/.tools/node-26/bin:$PATH"
+node tools/node-agent-api/build-viewer.mjs --novnc-root /absolute/path/to/novnc
+```
+
+产物位于本仓库 `.lab/node-agent-viewer/`,包含带内容哈希的 JS 和 manifest。CLI 启动时验证哈希;缺少产物时沿用原 noVNC 查看器。JS 可在已授权 lane 内缓存,HTML、授权和连接检查保持动态。容器镜像中的 noVNC 更新后重新构建产物,再重载 API。详细测试和第一期限制见 [VNC 第一阶段](../../docs/NODE_AGENT_VNC_PHASE_ONE.md)。
API 的授权保护 API 入口。原 GrokNode 的 6080/6081 VNC 端口保持原有访问策略,API 没有关闭或改写它们。API bot grants 也不构成共享 Linux 文件系统的 bot 间隔离。
diff --git a/tools/node-agent-api/bench/README.md b/tools/node-agent-api/bench/README.md
new file mode 100644
index 0000000..770ee08
--- /dev/null
+++ b/tools/node-agent-api/bench/README.md
@@ -0,0 +1,47 @@
+# node-agent-api bench
+
+Synthetic, same-machine measurement for the node-agent-api speed plan. No model calls, no live state, no Docker.
+
+## Fixtures
+
+- `fake-codex.mjs`: stdio JSON-RPC stand-in for the Codex `app-server` child. Answers `thread/start`, `thread/resume`, `thread/read`, `turn/start` locally; `BENCH_HARNESS_DELAY_MS` delays only the synthetic provider reply; `BENCH_STALL_METHOD` never answers one method (timeout path).
+- `fixture.mjs` `createFixture({ sourceRoot, ... })`: loads `server.mjs`, `codex.mjs`, `runtime/harness.mjs` from any source root (archived baseline or candidate checkout) by absolute path, then runs the real HTTP service, real Codex adapter, real Harness transport against a synthetic runtime (identity/ensure/status seam) and synthetic upstream. Counts `ensure`/`status`/`validateExecutor`/`turn/start`/`thread/resume`/atomic `sessions.json` renames; asserts zero real Docker calls by construction (the synthetic runtime has no Docker client).
+- `rfb.mjs` `createRfbFixture()`: protocol-compatible synthetic RFB upstream serving one known 64x48 solid-colour framebuffer (`37,149,211,255`). Counts connections, frames, key/pointer events.
+
+## Auth isolation
+
+The fixture writes a dummy private `auth.json` (`{}`) inside its own temporary directory and passes it as explicit `authFile` to `createHarness`. Archived sources whose harness predates the `authFile` option ignore the extra property and instead resolve auth through `homedir()`; benchmark workers therefore run in a forked child whose `HOME` is a fresh synthetic directory containing dummy auth JSON. The parent process environment is never mutated; `tests/node-agent-performance.test.mjs` asserts `process.env.HOME` is unchanged.
+
+## Commands (from the repository root R, Node 26.5.x on PATH)
+
+```sh
+export PATH="/Users/Apple/Documents/grokbot/.tools/node-26/bin:$PATH"
+node tools/node-agent-api/bench/performance.mjs --fixture --source-root /private/tmp/node-agent-speed-baseline-01a113f1 --cold-samples 30 --warm-samples 100 --output Q/task-1/baseline.json
+node tools/node-agent-api/bench/performance.mjs --fixture --source-root R --cold-samples 30 --warm-samples 100 --output Q/task-9/candidate.json
+node tools/node-agent-api/bench/browser.mjs --source-root /private/tmp/node-agent-speed-baseline-01a113f1 --novnc-root /private/tmp/node-agent-speed-novnc-01a113f1 --loader --cold-samples 30 --warm-samples 100 --output Q/task-1/browser.json
+node tools/node-agent-api/bench/performance.mjs --read-only-live --cold-samples 0 --warm-samples 2 --origin http://127.0.0.1:18770 --key-file --output Q/task-1/live.json
+```
+
+`--source-root` selects the measured source: the immutable archive for baseline, the working checkout for candidate. The report records sha256 of the eleven benchmark-relevant sources plus `git rev-parse HEAD` (null for archives) and re-hashes after the run (`sourceStable` false aborts that series).
+
+## Metrics
+
+`viewer-phase-one.mjs` validates the bundled embedded viewer using the real API, fake Codex, known framebuffer and isolated Chromium. Build the viewer first as described in the API README, then run:
+
+```sh
+node tools/node-agent-api/bench/viewer-phase-one.mjs --loader /absolute/path/to/omowright.mjs --output /absolute/path/to/private/evidence.json --samples 5
+```
+
+It records unprewarmed first open, prewarmed display and repeated display separately, checks decoded pixels, verifies reuse without new authorizations or upstream connections, and exercises selection cleanup, stale iframe messages, preserved control leases and explicit Control/Handback. Browser failures remain in the evidence; optional browser tests require `NODE_AGENT_VIEWER_BROWSER_LOADER` and do not download dependencies.
+
+- `localDispatchMs`: test client immediately before HTTP POST to the real Harness child-stdin `turn/start` write, same parent monotonic clock. Includes loopback transit; excludes the synthetic provider reply delay.
+- `httpAcceptedMs`: POST start to 202 accepted (excludes Harness reply).
+- `notificationToSseClientMs`: real Harness notification callback to SSE bytes observed by a loopback client, after asserting the event is already durable in `sessions.json`.
+- `firstFrameMs` (browser): capturing click handler on the real product View button to the first rAF observing matching noVNC-decoded visible canvas pixels (64x48, `37,149,211,255`). All browser interactions (key fill, connect, bot select, session and View presses) are dispatched as in-page DOM operations because the automation client's actionability/readiness waits intermittently time out on this live-updating page; timing still starts at the real capturing View-button handler, so the measured ticket/iframe/RFB/canvas path is unchanged. None of the setup steps are timed. Iframe `load` and WebSocket `open` are never counted as first paint.
+- `storeWrites`: successful atomic `sessions.json` renames per turn. `cpuMs` covers the parent process only.
+
+## Limits
+
+- Synthetic runtime identity/ensure/status seam: no real Docker or gateway timing. `--gateway-delay-ms N` adds a fixed, labelled delay per identity call, identically for baseline and candidate; it must never be presented as measured Docker time.
+- One active session, one delta per turn: not the 1/2/4-session 5000-delta stress matrix.
+- Live mode observes an already-running service with at most ten low-frequency reads; it cannot prove the deployed revision and claims no p95.
diff --git a/tools/node-agent-api/bench/browser.mjs b/tools/node-agent-api/bench/browser.mjs
new file mode 100644
index 0000000..a0823e6
--- /dev/null
+++ b/tools/node-agent-api/bench/browser.mjs
@@ -0,0 +1,170 @@
+import assert from 'node:assert/strict';
+import { parseArgs } from 'node:util';
+import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { fork } from 'node:child_process';
+import { createFixture, delay } from './fixture.mjs';
+import { createRfbFixture, pattern } from './rfb.mjs';
+import { sourceIdentity, writeEvidence, syntheticHome } from './performance.mjs';
+
+if (process.env.NODE_AGENT_BENCH_BROWSER_CHILD !== '1') {
+ const home = await syntheticHome();
+ try {
+ const code = await new Promise((resolve, reject) => {
+ const child = fork(import.meta.filename, process.argv.slice(2), { env: { ...process.env, HOME: home, NODE_AGENT_BENCH_BROWSER_CHILD: '1' }, stdio: 'inherit' });
+ child.once('error', reject); child.once('exit', resolve);
+ });
+ process.exitCode = code ?? 1;
+ } finally { await rm(home, { recursive: true, force: true }); }
+} else {
+
+const { values } = parseArgs({ options: { 'source-root': { type: 'string' }, 'novnc-root': { type: 'string' }, loader: { type: 'string' }, output: { type: 'string' }, 'cold-samples': { type: 'string', default: '30' }, 'warm-samples': { type: 'string', default: '100' }, 'rfb-delay-ms': { type: 'string', default: '0' } } });
+for (const name of ['source-root', 'novnc-root', 'loader', 'output']) assert.ok(values[name], '--' + name + ' required');
+const { loadOmowright } = await import(pathToFileURL(values.loader));
+const { omowright } = await loadOmowright();
+const report = { schema: 1, invocation: process.argv.slice(1), source: await sourceIdentity(values['source-root']), at: new Date().toISOString(), node: process.version, novnc: JSON.parse(await readFile(path.join(values['novnc-root'], 'package.json'), 'utf8')).version, viewport: { width: 1280, height: 900 }, pattern, samples: [], failures: [], cleanup: [], definitions: { cold: 'Fresh isolated Chrome profile and real API/Harness fixture, existing bound synthetic session; first viewer click.', warm: 'Same isolated profile/service, five complete viewer warmups then repeated viewer clicks.', firstFrameMs: 'Capturing click handler on real View button to first rAF observing matching noVNC-decoded visible canvas pixels, same page performance clock.', dispatch: 'All interactions as in-page DOM operations; automation-client actionability waits time out intermittently on this live page. Timing starts at the real capturing View handler; setup steps untimed.' }, limits: ['Real product UI, noVNC, desktop HTTP authorization and WebSocket/RFB filter; synthetic runtime identity and upstream framebuffer.', 'No model, real desktop content, live state or lease changes. Runtime Docker preparation timings are excluded.'] };
+const output = path.resolve(values.output);
+await writeEvidence(output, report);
+
+function summarizeSamples() { const out = {}; for (const kind of ['cold', 'warm']) { const rows = report.samples.filter(row => row.kind === kind).map(row => row.firstFrameMs).sort((a, b) => a - b); out[kind] = { samples: rows.length, p50: rows[Math.ceil(rows.length * .5) - 1] ?? null, p95: rows[Math.ceil(rows.length * .95) - 1] ?? null }; } return out; }
+
+const groupSpec = process.env.NODE_AGENT_BENCH_BROWSER_GROUP ? JSON.parse(process.env.NODE_AGENT_BENCH_BROWSER_GROUP) : null;
+if (groupSpec) {
+ const deliver = payload => new Promise(resolve => {
+ if (typeof process.send === 'function') { try { process.send(payload, resolve); return; } catch {} }
+ console.error('group-result ' + JSON.stringify(payload).slice(0, 2000));
+ resolve();
+ });
+ try {
+ await group(groupSpec.count, groupSpec.warmup, groupSpec.kind);
+ let firstFrame = null;
+ if (report.samples.length) { try { firstFrame = (await readFile(path.join(path.dirname(output), 'first-frame.png'))).toString('base64'); } catch {} }
+ await deliver({ ok: true, samples: report.samples, rfb: report.rfbCounters ?? {}, cleanup: report.cleanup, viewport: report.viewport, browser: report.browser, firstFrame });
+ } catch (error) {
+ await deliver({ ok: false, code: error.name, message: error.message, stack: String(error.stack).split('\n').slice(0, 8).join('\n') });
+ process.exitCode = 1;
+ }
+ if (process.connected) process.disconnect();
+ process.exit(process.exitCode ?? 0);
+}
+
+async function group(count, warmup, kind) {
+ const f = await createFixture({ sourceRoot: values['source-root'] });
+ const rfb = await createRfbFixture({ delayMs: Number(values['rfb-delay-ms']) });
+ const profile = await mkdtemp(path.join(tmpdir(), 'node-agent-bench-chrome-'));
+ let browser;
+ try {
+ f.runtime.desktop = async () => ({ object: 'node.environment', type: 'grok_node_codex', display: 3, websocketUrl: rfb.origin, containerGenerationImmutableId: 'synthetic-container', assignmentRevision: 'synthetic-assignment', containerStartedAt: 'synthetic-start', applicationId: null });
+ f.runtime.viewerAsset = async asset => { if (asset.split('/').includes('..')) throw new Error('Unsafe asset'); return readFile(path.join(values['novnc-root'], asset)); };
+ await f.session();
+ browser = await omowright.connectPipe({ browserPath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', browserArgs: ['--headless=new', '--no-first-run', '--disable-background-timer-throttling', '--user-data-dir=' + profile, '--window-size=1280,900'], storageRoot: profile });
+ const page = await browser.newTab(f.service.origin + '/ui/');
+ const describe = async () => ({ href: await page.evaluate('location.href'), ready: await page.evaluate('document.readyState'), hasKey: await page.evaluate("!!document.querySelector('#key')"), title: await page.evaluate('document.title'), body: String(await page.evaluate("document.body ? document.body.innerText : ''")).slice(0, 100) });
+ const wait = async expression => { for (let i = 0; i < 300; i++) { try { if (await page.evaluate(expression)) return; } catch {} await delay(30); } throw new Error('Browser condition timeout: ' + expression + ' at ' + JSON.stringify(await describe())); };
+ await page.goto(f.service.origin + '/ui/');
+ await wait("!!document.querySelector('#key')");
+ report.viewport = await page.evaluate('({ width: innerWidth, height: innerHeight, deviceScaleFactor: devicePixelRatio })');
+ // All setup interactions run as in-page DOM operations: the automation
+ // client's actionability/readiness waits intermittently time out on this
+ // live-updating page (NavigationReadinessTimeoutError), while the measured
+ // view activation is an in-page click whose timing starts at the real
+ // capturing handler. None of these setup steps are timed.
+ const act = expression => page.evaluate(expression);
+ await act(`{ const el = document.querySelector('#key'); el.value = ${JSON.stringify(f.key)}; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }`);
+ await act("document.querySelector('#connect').click()");
+ await wait("document.querySelector('#bots').options.length === 2");
+ await act("{ const s = document.querySelector('#bots'); s.value = 'bench-bot'; s.dispatchEvent(new Event('change', { bubbles: true })); }");
+ await wait("document.querySelectorAll('#sessions button').length === 1");
+ await act("document.querySelector('#sessions button').click()");
+ await wait("!document.querySelector('#view').disabled");
+ report.browser = await page.cdp.send('Browser.getVersion');
+ await page.evaluate(`(() => {
+ document.querySelector('#view').addEventListener('click', () => {
+ const previous = document.querySelector('#desktop iframe');
+ window.benchFrame = { start: performance.now(), done: false };
+ const probe = () => {
+ const iframe = document.querySelector('#desktop iframe');
+ const canvas = iframe && iframe !== previous && iframe.contentDocument?.querySelector('canvas');
+ if (canvas && canvas.width === ${pattern.width} && canvas.height === ${pattern.height} && canvas.getBoundingClientRect().width > 0) {
+ const pixel = Array.from(canvas.getContext('2d').getImageData(10, 10, 1, 1).data);
+ if (JSON.stringify(pixel) === '${JSON.stringify(pattern.rgba)}') { window.benchFrame = { done: true, ms: performance.now() - window.benchFrame.start, pixel }; return; }
+ }
+ requestAnimationFrame(probe);
+ }; requestAnimationFrame(probe);
+ }, { capture: true });
+ })()`);
+ for (let i = 0; i < count + warmup; i++) {
+ await wait("!document.querySelector('#view').disabled");
+ // In-page click dispatch: the iframe navigation trips the automation
+ // client's navigation-readiness wait; timing still starts at the real
+ // capturing View-button handler, so the measured path is unchanged.
+ await page.evaluate("document.querySelector('#view').click()");
+ await wait('window.benchFrame?.done === true');
+ const frame = await page.evaluate('window.benchFrame');
+ assert.deepEqual(frame.pixel, pattern.rgba);
+ if (i >= warmup) report.samples.push({ kind, firstFrameMs: frame.ms, pixel: frame.pixel });
+ if (report.samples.length === 1) {
+ await page.evaluate("document.querySelector('#desktop').scrollIntoView()");
+ await writeFile(path.join(path.dirname(output), 'first-frame.png'), await page.screenshot({ fullPage: true }), { mode: 0o600 });
+ }
+ }
+ report.rfbCounters = { ...rfb.counters };
+ report.summary = summarizeSamples();
+ await writeEvidence(output, report);
+ } finally {
+ if (browser) await browser.close();
+ await rm(profile, { recursive: true, force: true });
+ report.cleanup.push({ fixture: await f.close(), rfb: await rfb.close(), profileRemoved: true });
+ }
+}
+
+// Each measurement group runs in its own grandchild process: the automation
+// client's page-readiness wait intermittently raises an unhandled rejection
+// that escapes in-process try/catch and would otherwise lose the whole series.
+// A crashed group is recorded with its kind/index and the series continues.
+async function runGroup(count, warmup, kind, index) {
+ const trace = message => console.error(`[bench-parent ${new Date().toISOString()} ${kind}#${index}] ${message}`);
+ trace('forking group');
+ const grandchild = fork(import.meta.filename, process.argv.slice(2), { env: { ...process.env, NODE_AGENT_BENCH_BROWSER_GROUP: JSON.stringify({ count, warmup, kind }) }, stdio: ['ignore', 'ignore', 'pipe', 'ipc'] });
+ trace(`forked pid=${grandchild.pid} channel=${typeof grandchild.send}`);
+ let result, diagnostic = '';
+ const code = await new Promise(resolve => {
+ const timer = setTimeout(() => { trace('grandchild timeout, killing'); grandchild.kill(); resolve('timeout'); }, 300000);
+ grandchild.stderr.on('data', data => { diagnostic = (diagnostic + data).slice(-2000); });
+ grandchild.on('message', value => { trace(`message ok=${value?.ok}`); result = value; });
+ grandchild.on('error', error => { trace(`fork error: ${error.message}`); });
+ grandchild.on('exit', (exitCode, signal) => { clearTimeout(timer); trace(`exit code=${exitCode} signal=${signal} haveResult=${!!result}`); resolve(exitCode); });
+ });
+ trace(`settled code=${code}`);
+ if (code === 0 && result?.ok) {
+ report.samples.push(...result.samples);
+ report.cleanup.push(...result.cleanup);
+ if (result.viewport && !report.measuredViewport) { report.viewport = result.viewport; report.measuredViewport = true; }
+ if (result.browser && !report.measuredBrowser) { report.browser = result.browser; report.measuredBrowser = true; }
+ for (const [key, value] of Object.entries(result.rfb)) report.rfbCounters = { ...(report.rfbCounters ?? {}), [key]: (report.rfbCounters?.[key] ?? 0) + value };
+ if (result.firstFrame && !report.firstFrame) {
+ await writeFile(path.join(path.dirname(output), 'first-frame.png'), Buffer.from(result.firstFrame, 'base64'), { mode: 0o600 });
+ report.firstFrame = true;
+ }
+ } else {
+ report.failures.push({ kind, index, code: result?.ok === false ? result.code : 'group_crash', message: (result?.message ?? diagnostic).slice(0, 500) });
+ }
+ report.summary = summarizeSamples();
+ await writeEvidence(output, report);
+}
+process.on('unhandledRejection', error => { console.error('unhandled:', error?.message); process.exitCode = 1; });
+try {
+ for (let i = 0; i < Number(values['cold-samples']); i++) await runGroup(1, 0, 'cold', i);
+ if (Number(values['warm-samples'])) await runGroup(Number(values['warm-samples']), 5, 'warm', 0);
+} catch (error) { report.failures.push({ code: error.name, message: error.message, stack: String(error.stack).split('\n').slice(0, 6).join('\n') }); }
+report.summary = {};
+for (const kind of ['cold', 'warm']) { const rows = report.samples.filter(row => row.kind === kind).map(row => row.firstFrameMs).sort((a,b) => a-b); report.summary[kind] = { samples: rows.length, p50: rows[Math.ceil(rows.length * .5) - 1] ?? null, p95: rows[Math.ceil(rows.length * .95) - 1] ?? null }; }
+report.sourceAfter = await sourceIdentity(values['source-root']);
+report.sourceStable = JSON.stringify(report.source.hashes) === JSON.stringify(report.sourceAfter.hashes);
+if (!report.sourceStable) report.failures.push({ code: 'source_changed_during_measurement' });
+await writeEvidence(output, report);
+console.log(JSON.stringify({ artifact: output, summary: report.summary, failures: report.failures }));
+if (report.failures.length) process.exitCode = 1;
+}
diff --git a/tools/node-agent-api/bench/fake-codex.mjs b/tools/node-agent-api/bench/fake-codex.mjs
new file mode 100755
index 0000000..b9117e0
--- /dev/null
+++ b/tools/node-agent-api/bench/fake-codex.mjs
@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+import { createInterface } from 'node:readline';
+
+if (process.argv.includes('--version')) {
+ console.log('codex-cli 0.160.0');
+} else {
+ let thread = 0, turn = 0;
+ const send = value => process.stdout.write(JSON.stringify(value) + '\n');
+ createInterface({ input: process.stdin }).on('line', line => {
+ const message = JSON.parse(line);
+ if (!message.id) return;
+ let result = {};
+ if (message.method === 'thread/start') result = { thread: { id: 'bench-thread-' + ++thread } };
+ if (message.method === 'thread/resume') result = { thread: { id: message.params.threadId } };
+ if (message.method === 'thread/read') result = { thread: { turns: [] } };
+ if (message.method === 'turn/start') result = { turn: { id: 'bench-turn-' + ++turn, status: 'inProgress' } };
+ if (message.method === 'bench/notify') { send(message.params); return; }
+ if (message.method === process.env.BENCH_STALL_METHOD) return;
+ setTimeout(() => send({ id: message.id, result }), message.method === 'turn/start' ? Number(process.env.BENCH_HARNESS_DELAY_MS || 0) : 0);
+ });
+}
diff --git a/tools/node-agent-api/bench/fixture.mjs b/tools/node-agent-api/bench/fixture.mjs
new file mode 100644
index 0000000..3cea926
--- /dev/null
+++ b/tools/node-agent-api/bench/fixture.mjs
@@ -0,0 +1,158 @@
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { once } from 'node:events';
+import { mkdir, mkdtemp, readFile, realpath, rm, writeFile, chmod } from 'node:fs/promises';
+import { tmpdir, homedir } from 'node:os';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { performance } from 'node:perf_hooks';
+import fsPromises from 'node:fs/promises';
+import { syncBuiltinESMExports } from 'node:module';
+
+export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
+export async function createFixture({ sourceRoot, gatewayDelayMs = 0, harnessDelayMs = 0, stallMethod, timeoutMs = 2000, embeddedViewer = null } = {}) {
+ const load = file => import(pathToFileURL(path.join(sourceRoot, 'tools/node-agent-api', file)).href);
+ const [{ startNodeAgentApi }, { createCodexAdapter }, { createHarness }] = await Promise.all([load('server.mjs'), load('codex.mjs'), load('runtime/harness.mjs')]);
+ const directory = await realpath(await mkdtemp(path.join(tmpdir(), 'node-agent-bench-')));
+ await mkdir(path.join(directory, '.codex'), { mode: 0o700 });
+ await writeFile(path.join(directory, '.codex/auth.json'), '{}', { mode: 0o600 });
+ const binary = path.join(import.meta.dirname, 'fake-codex.mjs');
+ await chmod(binary, 0o700);
+ const counters = { ensure: 0, status: 0, validate: 0, turnStart: 0, threadResume: 0, spawn: 0, storeWrites: 0, realDockerCalls: 0, credentialShimLstat: 0, credentialShimSymlink: 0, credentialShimReadlink: 0, credentialLinksChecked: 0 };
+ const originalRename = fsPromises.rename;
+ fsPromises.rename = async (from, to) => { const result = await originalRename(from, to); if (to === path.join(directory, 'api/sessions.json')) counters.storeWrites++; return result; };
+ syncBuiltinESMExports();
+ // Archived sources predate the explicit authFile option: their Harness falls
+ // back to the process-homedir default auth path. Redirect exactly that path
+ // to the fixture dummy file (setup only; measured logic untouched). The auth
+ // file itself is only ever lstat-checked, never read, by prepareCodex.
+ const realDefaultAuth = path.resolve(homedir(), '.codex/auth.json');
+ const dummyAuth = path.join(directory, '.codex/auth.json');
+ const originalLstat = fsPromises.lstat, originalSymlink = fsPromises.symlink, originalReadlink = fsPromises.readlink;
+ const isDefaultAuth = value => typeof value === 'string' && path.resolve(value) === realDefaultAuth;
+ fsPromises.lstat = async value => isDefaultAuth(value) ? (counters.credentialShimLstat++, originalLstat(dummyAuth)) : originalLstat(value);
+ fsPromises.symlink = async (target, linkPath, type) => {
+ if (isDefaultAuth(target) && typeof linkPath === 'string' && linkPath.startsWith(directory + path.sep)) {
+ counters.credentialShimSymlink++;
+ return originalSymlink(dummyAuth, linkPath, type);
+ }
+ return originalSymlink(target, linkPath, type);
+ };
+ fsPromises.readlink = async (value, options) => {
+ const actual = await (options === undefined ? originalReadlink(value) : originalReadlink(value, options));
+ if (typeof value === 'string' && value.startsWith(directory + path.sep) && typeof actual === 'string' && path.resolve(path.dirname(value), actual) === dummyAuth) {
+ counters.credentialShimReadlink++;
+ return realDefaultAuth;
+ }
+ return actual;
+ };
+ syncBuiltinESMExports();
+ function restoreFs() { fsPromises.rename = originalRename; fsPromises.lstat = originalLstat; fsPromises.symlink = originalSymlink; fsPromises.readlink = originalReadlink; syncBuiltinESMExports(); }
+ async function auditCredentialLinks() {
+ let checked = 0; const violations = [];
+ async function walk(dir) {
+ for (const entry of await fsPromises.readdir(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name);
+ if (entry.isSymbolicLink()) {
+ checked++;
+ if (path.resolve(path.dirname(full), await originalReadlink(full)) === realDefaultAuth) violations.push({ link: full });
+ } else if (entry.isDirectory()) await walk(full);
+ }
+ }
+ await walk(directory);
+ return { symlinksChecked: checked, violations };
+ }
+ const stamps = { notifications: new Map(), turnDispatch: 0 };
+ const children = new Set();
+ const identity = { containerId: 'synthetic-container', containerStartedAt: 'synthetic-start', assignmentRevision: 'synthetic-assignment', display: 3 };
+ const runtime = {
+ shared: true,
+ async ensure() { counters.ensure++; await delay(gatewayDelayMs); return { running: true, ...identity }; },
+ async status() { counters.status++; await delay(gatewayDelayMs); return { running: true, exists: true, ...identity }; },
+ async validateExecutor() { counters.validate++; await delay(gatewayDelayMs); return identity; },
+ async descriptor() { return { version: 1, namespace: 'grok-node-lab-bench', environments: [{ transport: 'stdio', program: '/synthetic/never-executed', args: ['exec-server', 'stdio', '--no-new-privs'], ...identity }] }; },
+ async close() {},
+ };
+ const harness = createHarness({ runtime, namespace: 'grok-node-lab-bench', stateRoot: directory, binary, timeoutMs, authFile: path.join(directory, '.codex/auth.json'),
+ spawnProcess(_file, _args, options) {
+ counters.spawn++;
+ const child = spawn(process.execPath, [binary], { ...options, env: { ...options.env, BENCH_HARNESS_DELAY_MS: String(harnessDelayMs), BENCH_STALL_METHOD: stallMethod ?? '' } });
+ children.add(child); child.once('exit', () => children.delete(child));
+ const write = child.stdin.write.bind(child.stdin);
+ child.stdin.write = (data, ...args) => {
+ const message = JSON.parse(data);
+ if (message.method === 'turn/start') { counters.turnStart++; stamps.turnDispatch = performance.now(); }
+ if (message.method === 'thread/resume') counters.threadResume++;
+ return write(data, ...args);
+ };
+ return child;
+ },
+ });
+ harness.subscribe(({ message }) => {
+ const id = message.params?.itemId;
+ if (id) stamps.notifications.set(id, performance.now());
+ });
+ const bot = { object: 'node.agent', id: 'bench-bot', name: 'Synthetic benchmark', description: '', backend: 'codex_harness' };
+ const roster = { agents: async () => [bot], requireAgent: async id => { assert.equal(id, bot.id); return bot; } };
+ const adapter = createCodexAdapter({ runtime, harness, roster, namespace: 'grok-node-lab-bench', stateRoot: directory, artifactRoot: path.join(directory, 'artifacts') });
+ let store;
+ const attach = adapter.attachStore.bind(adapter);
+ adapter.attachStore = value => {
+ store = value;
+ return attach(value);
+ };
+ let service;
+ try { service = await startNodeAgentApi({ adapter, stateDirectory: path.join(directory, 'api'), governanceOptions: { limits: { requests: 10000 } }, embeddedViewer }); }
+ catch (error) { await harness.close(); restoreFs(); await rm(directory, { recursive: true, force: true }); throw error; }
+ const key = (await readFile(service.keyFile, 'utf8')).trim();
+ async function call(method, route, body) {
+ const response = await fetch(service.origin + route, { method, headers: { authorization: 'Bearer ' + key, ...(body ? { 'content-type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(10000) });
+ return { status: response.status, data: await response.json() };
+ }
+ return {
+ service, adapter, harness, runtime, counters, stamps, directory, key, call, get store() { return store; },
+ async session() { const reply = await call('POST', '/v1/agents/sessions', { agent_id: bot.id }); assert.equal(reply.status, 201); return reply.data; },
+ notify(message) { assert.equal(children.size, 1); [...children][0].stdin.write(JSON.stringify({ id: -1, method: 'bench/notify', params: message }) + '\n'); },
+ async close() {
+ const active = [...children];
+ const exited = active.map(child => once(child, 'exit'));
+ let credentialAudit = { symlinksChecked: 0, violations: [] };
+ try {
+ await service.close(); for (const child of active) if (child.exitCode === null && !child.killed) child.kill(); await Promise.all(exited);
+ credentialAudit = await auditCredentialLinks();
+ counters.credentialLinksChecked = credentialAudit.symlinksChecked;
+ if (credentialAudit.violations.length) throw new Error('Fixture credential audit failed: a fixture symlink targets the real user credential path');
+ }
+ finally { restoreFs(); await rm(directory, { recursive: true, force: true }); }
+ return { children: children.size, stateRemoved: true, credentialAudit };
+ },
+ };
+}
+
+export async function measureSample(f, session, index) {
+ const before = { ...f.counters }, cpu = process.cpuUsage();
+ const start = performance.now();
+ const accepted = await f.call('POST', `/v1/agents/sessions/${session.id}/events`, { events: [{ type: 'message', text: 'synthetic benchmark' }], idempotency_key: 'bench-' + index });
+ const acceptedAt = performance.now();
+ assert.equal(accepted.status, 202);
+ assert.equal(f.counters.turnStart - before.turnStart, 1);
+ const itemId = 'item-' + index;
+ const abort = new AbortController();
+ const response = await fetch(`${f.service.origin}/v1/agents/sessions/${session.id}/events`, { headers: { authorization: 'Bearer ' + f.key, accept: 'text/event-stream' }, signal: abort.signal });
+ assert.equal(response.status, 200);
+ const reader = response.body.getReader();
+ f.notify({ method: 'item/agentMessage/delta', params: { threadId: session.thread_id, turnId: accepted.data.turn_id, itemId, delta: 'x' } });
+ let text = '', receivedAt;
+ const timer = setTimeout(() => abort.abort(), 5000);
+ try {
+ while (!text.includes('"itemId":"' + itemId + '"')) { const part = await reader.read(); assert.equal(part.done, false); text += new TextDecoder().decode(part.value); }
+ receivedAt = performance.now();
+ const persisted = JSON.parse(await readFile(path.join(f.directory, 'api/sessions.json'), 'utf8'));
+ assert.ok(persisted.find(row => row.id === session.id).events.some(event => event.data?.params?.itemId === itemId));
+ } finally { clearTimeout(timer); abort.abort(); await reader.cancel().catch(() => {}); }
+ f.notify({ method: 'turn/completed', params: { threadId: session.thread_id, turn: { id: accepted.data.turn_id, status: 'completed' } } });
+ for (let attempt = 0; attempt < 1000 && f.store.get(session.id).task_status === 'running'; attempt++) { await delay(2); await f.adapter.flush(); }
+ assert.equal(f.store.get(session.id).task_status, 'completed');
+ const used = process.cpuUsage(cpu);
+ return { localDispatchMs: f.stamps.turnDispatch - start, httpAcceptedMs: acceptedAt - start, notificationToSseClientMs: receivedAt - f.stamps.notifications.get(itemId), cpuMs: (used.user + used.system) / 1000, rssBytes: process.memoryUsage().rss, counters: Object.fromEntries(Object.keys(before).map(key => [key, f.counters[key] - before[key]])), durableBeforeObserved: true, exactlyOneTurn: true };
+}
diff --git a/tools/node-agent-api/bench/performance.mjs b/tools/node-agent-api/bench/performance.mjs
new file mode 100644
index 0000000..9bddf61
--- /dev/null
+++ b/tools/node-agent-api/bench/performance.mjs
@@ -0,0 +1,136 @@
+import { parseArgs } from 'node:util';
+import { fork, execFileSync } from 'node:child_process';
+import { mkdir, mkdtemp, readFile, writeFile, chmod, rm, realpath } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import assert from 'node:assert/strict';
+import { createHash } from 'node:crypto';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { createFixture, measureSample, delay } from './fixture.mjs';
+
+export function summarize(rows) {
+ const metrics = {};
+ for (const name of ['localDispatchMs', 'httpAcceptedMs', 'notificationToSseClientMs', 'cpuMs', 'rssBytes']) {
+ const values = rows.map(row => row[name]).filter(Number.isFinite).sort((a, b) => a - b);
+ metrics[name] = { samples: values.length, p50: values[Math.ceil(values.length * .5) - 1] ?? null, p95: values[Math.ceil(values.length * .95) - 1] ?? null, min: values[0] ?? null, max: values.at(-1) ?? null };
+ }
+ return metrics;
+}
+
+export async function sourceIdentity(root) {
+ const names = ['server.mjs', 'codex.mjs', 'runtime/harness.mjs', 'runtime/codex.mjs', 'store.mjs', 'persistence.mjs', 'shared-runtime.mjs', 'grok-node.mjs', 'desktop.mjs', 'web/app.js', 'web/client.js'];
+ const hashes = {};
+ for (const name of names) hashes[name] = createHash('sha256').update(await readFile(path.join(root, 'tools/node-agent-api', name))).digest('hex');
+ let revision = null;
+ try { revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { /* Archives are identified by file hashes. */ }
+ return { root, revision, hashes };
+}
+
+export async function writeEvidence(file, data) {
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
+ await chmod(path.dirname(file), 0o700);
+ await writeFile(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
+ await chmod(file, 0o600);
+}
+
+async function worker(options) {
+ const f = await createFixture(options);
+ const rows = [], failures = [];
+ let cleanup;
+ try {
+ const session = await f.session();
+ if (options.scenario === 'timeout') {
+ const route = `/v1/agents/sessions/${session.id}/events`;
+ const body = { events: [{ type: 'message', text: 'synthetic timeout' }], idempotency_key: 'bench-timeout' };
+ assert.equal((await f.call('POST', route, body)).status, 500);
+ await f.call('POST', route, body);
+ assert.equal(f.counters.turnStart, 1);
+ assert.equal(f.store.get(session.id).requests['bench-timeout'].status, 'unknown');
+ rows.push({ unknown: true, turnStart: f.counters.turnStart });
+ }
+ for (let index = 0; index < options.warmup + options.samples; index++) {
+ try { const row = await measureSample(f, session, index); if (index >= options.warmup) rows.push(row); }
+ catch (error) { failures.push({ sample: index, code: error.code ?? error.name }); break; }
+ }
+ if (options.scenario === 'replay') {
+ const replay = await f.call('POST', `/v1/agents/sessions/${session.id}/events`, { events: [{ type: 'message', text: 'synthetic benchmark' }], idempotency_key: 'bench-0' });
+ assert.equal(replay.status, 202); assert.equal(replay.data.replayed, true); assert.equal(f.counters.turnStart, 1);
+ }
+ } finally { cleanup = await f.close(); }
+ return { rows, failures, cleanup };
+}
+
+export async function runWorker(options) {
+ const home = await syntheticHome();
+ try { return await new Promise((resolve, reject) => {
+ const child = fork(fileURLToPath(import.meta.url), ['--worker'], { env: { ...process.env, HOME: home }, stdio: ['ignore', 'ignore', 'pipe', 'ipc'] });
+ let result, diagnostic = '';
+ const timer = setTimeout(() => { child.kill(); reject(new Error('Benchmark worker timeout')); }, 180000);
+ child.stderr.on('data', data => { diagnostic = (diagnostic + data).slice(-2000); });
+ child.on('message', value => { result = value; });
+ child.on('error', reject);
+ child.on('exit', code => { clearTimeout(timer); if (code === 0 && result) resolve(result); else reject(new Error('Benchmark worker failed: ' + diagnostic)); });
+ child.send(options);
+ }); } finally { await rm(home, { recursive: true, force: true }); }
+}
+
+export async function syntheticHome() {
+ const home = await realpath(await mkdtemp(path.join(tmpdir(), 'node-agent-bench-home-')));
+ await mkdir(path.join(home, '.codex'), { mode: 0o700 });
+ await writeFile(path.join(home, '.codex/auth.json'), '{}', { mode: 0o600 });
+ return home;
+}
+
+async function main() {
+ const { values } = parseArgs({ options: { fixture: { type: 'boolean' }, 'read-only-live': { type: 'boolean' }, 'source-root': { type: 'string', default: path.resolve(import.meta.dirname, '../../..') }, 'cold-samples': { type: 'string', default: '30' }, 'warm-samples': { type: 'string', default: '100' }, output: { type: 'string' }, origin: { type: 'string', default: 'http://127.0.0.1:18770' }, 'key-file': { type: 'string' }, 'gateway-delay-ms': { type: 'string', default: '0' }, 'harness-delay-ms': { type: 'string', default: '0' } } });
+ if (!values.output || Boolean(values.fixture) === Boolean(values['read-only-live'])) throw new Error('Choose --fixture or --read-only-live and --output');
+ const cold = Number(values['cold-samples']), warm = Number(values['warm-samples']);
+ if (![cold, warm].every(value => Number.isInteger(value) && value >= 0 && value <= 1000) || cold + warm === 0) throw new Error('Invalid sample count');
+ const report = { schema: 1, at: new Date().toISOString(), invocation: process.argv.slice(1), node: process.version, platform: process.platform, architecture: process.arch, source: await sourceIdentity(values['source-root']), requested: { cold, warm }, failures: [], mode: values.fixture ? 'fixture' : 'read-only-live' };
+ if (values['read-only-live']) {
+ const origin = new URL(values.origin);
+ if (origin.origin !== values.origin || origin.hostname !== '127.0.0.1' || origin.protocol !== 'http:' || origin.username || origin.password) throw new Error('Loopback origin required');
+ if (cold + warm > 10 || cold !== 0) throw new Error('Live reads require --cold-samples 0 and at most 10 warm samples; live cold cannot be controlled');
+ const key = values['key-file'] ? (await readFile(values['key-file'], 'utf8')).trim() : null;
+ report.definition = 'Low-frequency GET /health and authenticated GET /v1/agents only. No bodies, identifiers, credentials, viewer tickets, or model requests recorded.';
+ report.observations = [];
+ for (let i = 0; i < warm; i++) {
+ for (const route of key ? ['/health', '/v1/agents'] : ['/health']) {
+ const start = performance.now();
+ const response = await fetch(values.origin + route, { headers: key ? { authorization: 'Bearer ' + key } : {}, signal: AbortSignal.timeout(30000) });
+ const bytes = (await response.arrayBuffer()).byteLength;
+ report.observations.push({ route, status: response.status, ms: performance.now() - start, bytes });
+ if (!response.ok) report.failures.push({ route, status: response.status });
+ }
+ if (i + 1 < warm) await delay(1000);
+ }
+ report.limits = ['Observes already-running service; sourceRoot does not prove deployed revision.', 'At most ten samples; no high-confidence p95 or cold claim.'];
+ } else {
+ const options = { sourceRoot: values['source-root'], gatewayDelayMs: Number(values['gateway-delay-ms']), harnessDelayMs: Number(values['harness-delay-ms']) };
+ report.configuration = options;
+ report.configuration.fixtureRequestsPerMinute = 10000;
+ report.configuration.authIsolation = 'Worker child env.HOME is a fresh synthetic directory containing empty auth JSON; parent environment unchanged. Candidate also receives explicit fixture authFile. Archived sources without authFile get a setup-only lstat/symlink/readlink redirect of exactly the default auth path to the fixture dummy (never read); close() audits every fixture symlink and fails on any target resolving to the real user credential path.';
+ report.definitions = { cold: 'Fresh Node process, fresh private state, one synthetic session/thread; first turn after bind. Does not include process start or session bind.', warm: 'One process/session, five completed warmup turns, then serial measured turns; history grows identically in both revisions.', localDispatchMs: 'Client immediately before HTTP POST to real Harness child stdin turn/start write, same parent monotonic clock; includes loopback transit; excludes synthetic provider reply delay.', notificationToSseClientMs: 'Real Harness notification callback to SSE bytes observed by loopback client; includes persistence and network, not exact server write.', storeWrites: 'Successful rename of sessions.json atomic snapshots; no persistence bypass.', cpuMs: 'Parent process CPU only, child excluded.' };
+ report.limits = ['Synthetic runtime identity/ensure/status seam; no real Docker or gateway timing.', 'Real HTTP, governance, adapter, store fsync/rename, Harness JSON-RPC child transport; fake Codex version endpoint, no model invocation.', 'Browser/RFB measurement is a separate artifact; iframe load and WebSocket open are not first paint.', 'This series is one active session and one delta per turn, not the 1/2/4-session 5000-delta stress matrix.'];
+ report.cold = []; report.warm = []; report.cleanup = [];
+ for (let i = 0; i < cold; i++) {
+ const result = await runWorker({ ...options, warmup: 0, samples: 1 });
+ report.cold.push(...result.rows); report.failures.push(...result.failures); report.cleanup.push(result.cleanup);
+ }
+ if (warm) { const result = await runWorker({ ...options, warmup: 5, samples: warm }); report.warm.push(...result.rows); report.failures.push(...result.failures); report.cleanup.push(result.cleanup); }
+ report.credentialAudit = { workers: report.cleanup.length, symlinksChecked: report.cleanup.reduce((n, entry) => n + (entry.credentialAudit?.symlinksChecked ?? 0), 0), violations: report.cleanup.flatMap(entry => entry.credentialAudit?.violations ?? []), shimSymlinkRedirects: [...report.cold, ...report.warm].reduce((n, row) => n + (row.counters?.credentialShimSymlink ?? 0), 0) };
+ if (report.credentialAudit.violations.length) report.failures.push({ code: 'credential_audit' });
+ report.summary = { cold: summarize(report.cold), warm: summarize(report.warm) };
+ report.failureRate = report.failures.length / (cold + warm);
+ report.sourceAfter = await sourceIdentity(values['source-root']);
+ report.sourceStable = JSON.stringify(report.source.hashes) === JSON.stringify(report.sourceAfter.hashes);
+ if (!report.sourceStable) report.failures.push({ code: 'source_changed_during_measurement' });
+ }
+ await writeEvidence(path.resolve(values.output), report);
+ console.log(JSON.stringify({ artifact: path.resolve(values.output), failures: report.failures.length, summary: report.summary }));
+ if (report.failures.length) process.exitCode = 1;
+}
+
+if (process.argv[2] === '--worker') {
+ process.once('message', async options => { try { process.send(await worker(options)); process.disconnect(); } catch (error) { console.error(error.stack); process.exitCode = 1; process.disconnect(); } });
+} else if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
diff --git a/tools/node-agent-api/bench/rfb.mjs b/tools/node-agent-api/bench/rfb.mjs
new file mode 100644
index 0000000..29cce32
--- /dev/null
+++ b/tools/node-agent-api/bench/rfb.mjs
@@ -0,0 +1,53 @@
+import { createServer } from 'node:http';
+import { WebSocketServer } from 'ws';
+
+export const pattern = { width: 64, height: 48, rgba: [37, 149, 211, 255] };
+export async function createRfbFixture({ delayMs = 0, stall = false } = {}) {
+ const server = createServer();
+ const wss = new WebSocketServer({ server });
+ const counters = { connections: 0, frames: 0, keyEvents: 0, pointerEvents: 0 };
+ const timers = new Set();
+ wss.on('connection', socket => {
+ counters.connections++;
+ let stage = 0, pending = Buffer.alloc(0), sent = false;
+ socket.send(Buffer.from('RFB 003.008\n'));
+ socket.on('message', bytes => {
+ pending = Buffer.concat([pending, bytes]);
+ while (pending.length) {
+ let size = stage === 0 ? 12 : stage < 3 ? 1 : null;
+ if (stage === 3) {
+ if (pending[0] === 0) size = 20;
+ else if (pending[0] === 2) size = pending.length >= 4 ? 4 + pending.readUInt16BE(2) * 4 : null;
+ else if (pending[0] === 3) size = 10;
+ else if (pending[0] === 4) size = 8;
+ else if (pending[0] === 5) size = 6;
+ else { socket.close(); return; }
+ }
+ if (size === null || pending.length < size) return;
+ const message = pending.subarray(0, size); pending = pending.subarray(size);
+ if (stage === 0) { socket.send(Buffer.from([1, 1])); stage++; }
+ else if (stage === 1) { socket.send(Buffer.alloc(4)); stage++; }
+ else if (stage === 2) {
+ const name = Buffer.from('Known synthetic framebuffer');
+ const init = Buffer.alloc(24 + name.length);
+ init.writeUInt16BE(pattern.width, 0); init.writeUInt16BE(pattern.height, 2);
+ init[4] = 32; init[5] = 24; init[7] = 1;
+ init.writeUInt16BE(255, 8); init.writeUInt16BE(255, 10); init.writeUInt16BE(255, 12);
+ init[14] = 16; init[15] = 8; init[16] = 0;
+ init.writeUInt32BE(name.length, 20); name.copy(init, 24); socket.send(init); stage++;
+ } else if (message[0] === 4) counters.keyEvents++;
+ else if (message[0] === 5) counters.pointerEvents++;
+ else if (message[0] === 3 && !sent && !stall) {
+ sent = true;
+ const frame = Buffer.alloc(16 + pattern.width * pattern.height * 4);
+ frame[3] = 1; frame.writeUInt16BE(pattern.width, 8); frame.writeUInt16BE(pattern.height, 10);
+ for (let i = 16; i < frame.length; i += 4) { frame[i] = pattern.rgba[0]; frame[i + 1] = pattern.rgba[1]; frame[i + 2] = pattern.rgba[2]; }
+ const timer = setTimeout(() => { timers.delete(timer); if (socket.readyState === 1) { counters.frames++; socket.send(frame); } }, delayMs);
+ timers.add(timer);
+ }
+ }
+ });
+ });
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
+ return { origin: 'ws://127.0.0.1:' + server.address().port, counters, async close() { for (const timer of timers) clearTimeout(timer); for (const client of wss.clients) client.terminate(); await new Promise(resolve => wss.close(resolve)); await new Promise(resolve => server.close(resolve)); return { sockets: wss.clients.size, timers: timers.size }; } };
+}
diff --git a/tools/node-agent-api/bench/viewer-phase-one.mjs b/tools/node-agent-api/bench/viewer-phase-one.mjs
new file mode 100644
index 0000000..c8f5ba9
--- /dev/null
+++ b/tools/node-agent-api/bench/viewer-phase-one.mjs
@@ -0,0 +1,390 @@
+import assert from 'node:assert/strict';
+import { fork } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+import { parseArgs } from 'node:util';
+import { createFixture, delay } from './fixture.mjs';
+import { createRfbFixture, pattern } from './rfb.mjs';
+import { sourceIdentity, syntheticHome, writeEvidence } from './performance.mjs';
+
+export const kinds = ['no-prewarm-first-open', 'prewarmed-open', 'repeat-open'];
+
+export function summarizeSamples(samples) {
+ return Object.fromEntries(kinds.map(kind => {
+ const values = samples.filter(row => row.kind === kind).map(row => row.firstPixelMs).sort((a, b) => a - b);
+ return [kind, { samples: values.length, p50: values[Math.ceil(values.length * .5) - 1] ?? null, p95: values[Math.ceil(values.length * .95) - 1] ?? null }];
+ }));
+}
+
+export function verifyPixel(frame) {
+ assert.equal(frame.width, pattern.width, 'real framebuffer width');
+ assert.equal(frame.height, pattern.height, 'real framebuffer height');
+ assert.deepEqual(frame.pixel, pattern.rgba, 'known RFB pattern decoded by the built noVNC bundle');
+}
+
+export function verifyReuse(before, after) {
+ assert.equal(after.frameId, before.frameId, 'View must retain the same iframe');
+ assert.equal(after.authorizations, before.authorizations, 'View must not authorize again');
+ assert.equal(after.connections, before.connections, 'View must not open another upstream socket');
+}
+
+export function failureKind(error) {
+ if (/SyntaxError|wasn't found|Raw channel missing property/.test(error.name + ' ' + error.message)) return 'benchmark';
+ if (/listen EPERM|ENOENT/.test(error.message)) return 'environment';
+ return /NavigationReadinessTimeout|BrowserClosed|TargetClosed|PipeClosed|CDP.*(?:closed|timeout)|readiness.*timeout/i.test(error.name + ' ' + error.message)
+ ? 'browser-runtime' : 'product-or-contract';
+}
+
+// Installed before any product script. These observers retain native fetch and
+// WebSocket behavior; the one response gate is used only for the cancellation race.
+function instrumentation(expected) {
+ window.__phaseSockets = [];
+ window.__phaseErrors = [];
+ addEventListener('error', event => window.__phaseErrors.push({ message: event.message, stack: event.error?.stack }));
+ const Socket = window.WebSocket;
+ window.WebSocket = new Proxy(Socket, {
+ construct(target, args) { const socket = Reflect.construct(target, args); window.__phaseSockets.push(socket); return socket; },
+ });
+ if (window !== window.top) return;
+ const b = window.__phase = { requests: [], messages: [], clicks: [], nextFrame: 0, gate: null };
+ const ids = new WeakMap();
+ b.id = frame => { if (!frame) return null; if (!ids.has(frame)) ids.set(frame, ++b.nextFrame); return ids.get(frame); };
+ b.pixel = frame => {
+ try {
+ const canvas = frame?.contentDocument?.querySelector('canvas');
+ if (!canvas || canvas.width !== expected.width || canvas.height !== expected.height) return null;
+ return { width: canvas.width, height: canvas.height, pixel: Array.from(canvas.getContext('2d').getImageData(10, 10, 1, 1).data) };
+ } catch { return null; }
+ };
+ const nativeFetch = window.fetch;
+ window.fetch = async (...args) => {
+ const url = new URL(typeof args[0] === 'string' ? args[0] : args[0].url, location.href);
+ const options = args[1] ?? {};
+ if (url.origin !== location.origin || !/\/(desktop|handback|close)$/.test(url.pathname)) return nativeFetch(...args);
+ const row = { path: url.pathname, method: options.method ?? 'GET', at: performance.now(), aborted: false };
+ if (typeof options.body === 'string' && options.body.startsWith('{')) row.body = JSON.parse(options.body);
+ b.requests.push(row);
+ options.signal?.addEventListener('abort', () => { row.aborted = true; }, { once: true });
+ try {
+ const response = await nativeFetch(...args);
+ row.status = response.status; row.responseAt = performance.now();
+ if (b.gate && row.body?.existing_only === true) {
+ const gate = b.gate; row.held = true;
+ await new Promise(resolve => { gate.release = resolve; });
+ row.released = true;
+ }
+ row.deliveredAt = performance.now();
+ return response;
+ } catch (error) { row.error = error.name; throw error; }
+ };
+ addEventListener('message', event => {
+ if (event.data?.source !== 'node-agent-viewer') return;
+ const frame = document.querySelector('#desktop iframe');
+ const current = event.origin === location.origin && event.source === frame?.contentWindow;
+ b.messages.push({ type: event.data.type, mode: event.data.mode, at: performance.now(), trusted: event.isTrusted,
+ current, frameId: b.id(frame), ...(current && event.data.type === 'first-frame' ? { framebuffer: b.pixel(frame) } : {}) });
+ });
+ document.addEventListener('click', event => {
+ if (!['view', 'control', 'handback'].includes(event.target.id) || event.target.disabled) return;
+ const row = { button: event.target.id, at: performance.now(), done: false };
+ b.clicks.push(row);
+ const probe = () => {
+ const frame = document.querySelector('#desktop iframe'), pixel = b.pixel(frame);
+ if (frame && !frame.hidden && frame.getBoundingClientRect().width > 0 && pixel && JSON.stringify(pixel.pixel) === JSON.stringify(expected.rgba)) {
+ Object.assign(row, { done: true, firstPixelMs: performance.now() - row.at, frameId: b.id(frame), ...pixel });
+ } else if (performance.now() - row.at < 15000) requestAnimationFrame(probe);
+ };
+ // The product click handler runs before this animation-frame observation.
+ requestAnimationFrame(probe);
+ }, true);
+}
+
+async function sourceWithViewer(root) {
+ const identity = await sourceIdentity(root);
+ for (const name of ['viewer.mjs', 'web/viewer.js', 'web/viewer-frame.js']) {
+ identity.hashes[name] = createHash('sha256').update(await readFile(path.join(root, 'tools/node-agent-api', name))).digest('hex');
+ }
+ const directory = path.join(root, '.lab/node-agent-viewer');
+ identity.bundle = JSON.parse(await readFile(path.join(directory, 'manifest.json'), 'utf8'));
+ identity.bundle.sha256 = createHash('sha256').update(await readFile(path.join(directory, identity.bundle.file))).digest('hex');
+ assert.equal(identity.bundle.file, `viewer-${identity.bundle.sha256}.js`);
+ return identity;
+}
+
+async function runGroup({ root, omowright, browserPath, output, index, report }) {
+ const { loadEmbeddedViewer } = await import(pathToFileURL(path.join(root, 'tools/node-agent-api/viewer.mjs')));
+ const embeddedViewer = loadEmbeddedViewer({ directory: path.join(root, '.lab/node-agent-viewer') });
+ assert.ok(embeddedViewer, 'Build .lab/node-agent-viewer before running this benchmark');
+ const profile = await mkdtemp(path.join(tmpdir(), 'viewer-phase-one-chrome-'));
+ const group = { index, checks: [], samples: [], artifacts: [], runtimeDesktop: [] };
+ report.groups.push(group);
+ let f, rfb, browser, page;
+ let stage = 'setup';
+ const save = () => writeEvidence(output, report);
+ const check = async (scenario, observed) => { group.checks.push({ scenario, pass: true, observed }); await save(); };
+ try {
+ f = await createFixture({ sourceRoot: root, embeddedViewer });
+ rfb = await createRfbFixture({ delayMs: 100 });
+ f.runtime.desktop = async (_id, _target, mode, options) => {
+ group.runtimeDesktop.push({ mode, prepare: options.prepare });
+ return { object: 'node.environment', type: 'grok_node_codex', display: 3, websocketUrl: rfb.origin,
+ containerGenerationImmutableId: 'synthetic-container', assignmentRevision: 'synthetic-assignment', containerStartedAt: 'synthetic-start', applicationId: null };
+ };
+ f.runtime.viewerAsset = async () => { throw new Error('Embedded benchmark must never use fallback viewer assets'); };
+ const first = await f.session(), second = await f.session();
+ browser = await omowright.connectPipe({ browserPath, browserArgs: ['--headless=new', '--no-first-run', '--disable-background-timer-throttling', '--user-data-dir=' + profile, '--window-size=1280,900'], storageRoot: profile });
+ page = await browser.newTab('about:blank');
+ await page.cdp.send('Page.addScriptToEvaluateOnNewDocument', { source: `(${instrumentation.toString()})(${JSON.stringify(pattern)})` }, await page.resolveSessionId());
+ group.wire = [];
+ await page.cdp.send('Network.enable', {}, await page.resolveSessionId());
+ for (const event of ['Network.webSocketFrameSent', 'Network.webSocketFrameReceived', 'Network.webSocketFrameError']) {
+ page.cdp.on(event, data => group.wire.push({ event, opcode: data.response?.opcode, payload: data.response?.payloadData?.slice(0, 80), error: data.errorMessage }));
+ }
+ report.browser ??= await page.cdp.send('Browser.getVersion');
+ const evaluate = expression => page.evaluate(expression);
+ const wait = async (expression, label = expression) => {
+ const deadline = Date.now() + 15000;
+ while (Date.now() < deadline) {
+ if (await evaluate(expression)) return;
+ await delay(25);
+ }
+ throw new Error('Product condition timeout: ' + label);
+ };
+ const state = async () => ({ ...await evaluate(`({ frameId: __phase.id(document.querySelector('#desktop iframe')), authorizations: __phase.requests.filter(r => r.path.endsWith('/desktop')).length })`), connections: rfb.counters.connections });
+ const click = async button => {
+ await wait(`!document.querySelector('#${button}').disabled`);
+ await evaluate(`document.querySelector('#${button}').click()`);
+ await wait('__phase.clicks.at(-1)?.done === true', button + ' visible decoded pixel');
+ const row = await evaluate('__phase.clicks.at(-1)');
+ verifyPixel(row);
+ return row;
+ };
+ const capture = async name => {
+ await evaluate("document.querySelector('#desktop').scrollIntoView()");
+ const file = output.replace(/\.json$/, '') + `-${index}-${name}.png`;
+ await writeFile(file, await page.screenshot({ fullPage: true }), { mode: 0o600 });
+ assert.ok((await readFile(file)).length > 0);
+ group.artifacts.push(file);
+ };
+ const select = id => evaluate(`document.querySelector('#sessions button[title=${JSON.stringify(id)}]').click()`);
+ const remember = () => evaluate(`(() => { const frame = document.querySelector('#desktop iframe'); __phase.oldFrame = frame; __phase.oldWindow = frame.contentWindow; __phase.oldSockets = [...frame.contentWindow.__phaseSockets]; __phase.oldLane = frame.contentWindow.location.pathname.replace('vnc.html', ''); })()`);
+ const observeDisposal = async label => {
+ await wait('__phase.oldSockets.length > 0 && __phase.oldSockets.every(socket => socket.readyState === 3)', label + ' old socket closed');
+ const closed = await evaluate(`fetch(__phase.oldLane + 'mandatory.json').then(response => response.status)`);
+ assert.equal(closed, 401, label + ' old lane revoked');
+ return { socketStates: await evaluate('__phase.oldSockets.map(socket => socket.readyState)'), oldLaneStatus: closed };
+ };
+ const open = async () => {
+ await page.goto(f.service.origin + '/ui/');
+ await wait("!!document.querySelector('#key')");
+ await evaluate(`{ const key = document.querySelector('#key'); key.value = ${JSON.stringify(f.key)}; key.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('#connect').click(); }`);
+ await wait("document.querySelector('#bots').options.length === 2");
+ await delay(450);
+ assert.equal((await state()).authorizations, 0, 'key entry alone cannot prewarm');
+ await evaluate("{ const bot = document.querySelector('#bots'); bot.value = 'bench-bot'; bot.dispatchEvent(new Event('change', { bubbles: true })); }");
+ await wait("document.querySelectorAll('#sessions button').length === 2");
+ };
+ stage = 'no-prewarm-first-open';
+ await open();
+ // Activate the real View button in the first enabled microtask, before the
+ // product's normal prewarm timer; do not alter product timers or code.
+ await evaluate(`(() => { const view = document.querySelector('#view'); const observer = new MutationObserver(() => { if (!view.disabled) { observer.disconnect(); view.click(); } }); observer.observe(view, { attributes: true, attributeFilter: ['disabled'] }); })()`);
+ await select(first.id);
+ await wait('__phase.clicks.at(-1)?.done === true', 'first visible decoded framebuffer');
+ const firstPixel = await evaluate('__phase.clicks.at(-1)');
+ verifyPixel(firstPixel);
+ await wait("__phase.messages.some(m => m.type === 'first-frame' && m.current && m.trusted)");
+ const firstFrame = await evaluate("__phase.messages.find(m => m.type === 'first-frame' && m.current && m.trusted)");
+ verifyPixel(firstFrame.framebuffer);
+ const initialRequests = await evaluate('__phase.requests');
+ assert.equal(initialRequests.filter(row => row.path.endsWith('/desktop')).length, 1);
+ assert.equal(initialRequests[0].body.existing_only, undefined);
+ assert.equal(rfb.counters.frames, 1);
+ assert.equal(rfb.counters.connections, 1);
+ const assets = await evaluate("Array.from(document.querySelector('#desktop iframe').contentDocument.scripts).filter(s => s.src).map(s => new URL(s.src).pathname.split('/').at(-1))");
+ assert.deepEqual(assets, [report.source.bundle.file]);
+ group.samples.push({ kind: stage, ...firstPixel, firstFrameMessageMs: firstFrame.at - firstPixel.at });
+ await capture('first-frame');
+ await check('genuine first framebuffer, pixel and built embedded bundle', { firstFrame, frames: rfb.counters.frames, assets, initialRequests });
+
+ stage = 'repeat-open';
+ const beforeRepeat = await state(), repeated = await click('view');
+ await delay(450);
+ const afterRepeat = await state();
+ verifyReuse(beforeRepeat, afterRepeat);
+ group.samples.push({ kind: stage, ...repeated });
+ await check('View reuses connected iframe without authorization or upstream connection', { beforeRepeat, afterRepeat });
+
+ stage = 'selection-disposes';
+ await remember();
+ await select(second.id);
+ const disposal = await observeDisposal('selection');
+ await wait("__phase.messages.some(m => m.type === 'first-frame' && m.current && m.frameId !== __phase.id(__phase.oldFrame))", 'new selection prewarm real framebuffer');
+ await check('session selection disposes old RFB and revokes old lane', disposal);
+
+ stage = 'stale-iframe-messages';
+ const beforeStale = await state();
+ const statusBefore = await evaluate("document.querySelector('#desktop-status').textContent");
+ await evaluate(`(() => { for (const type of ['disconnected', 'ready', 'connected', 'first-frame']) dispatchEvent(new MessageEvent('message', { origin: location.origin, source: __phase.oldWindow, data: { source: 'node-agent-viewer', type, mode: 'view', expiresAt: 1 } })); })()`);
+ await delay(100);
+ verifyReuse(beforeStale, await state());
+ assert.equal(await evaluate("document.querySelector('#desktop-status').textContent"), statusBefore);
+ await click('view');
+ verifyReuse(beforeStale, await state());
+ await check('detached old iframe messages cannot change or dispose replacement', { beforeStale, after: await state(), statusUnchanged: true });
+
+ stage = 'prewarmed-open';
+ // A separate synthetic control lease must survive background prewarming.
+ const issued = await f.call('POST', `/v1/agents/sessions/${first.id}/desktop`, { mode: 'control', replace_own_control: true });
+ assert.equal(issued.status, 201);
+ const redeemed = await fetch(issued.data.url, { redirect: 'manual' });
+ assert.equal(redeemed.status, 303);
+ const cookie = redeemed.headers.get('set-cookie').split(';')[0];
+ const mandatory = new URL(redeemed.headers.get('location').replace('vnc.html', 'mandatory.json'), f.service.origin);
+ const controlStatus = async () => {
+ const response = await fetch(mandatory, { headers: { cookie } });
+ return { status: response.status, body: await response.json() };
+ };
+ assert.equal((await controlStatus()).body.view_only, false);
+ await open();
+ const desktopBefore = group.runtimeDesktop.length;
+ await select(first.id);
+ await wait("__phase.messages.some(m => m.type === 'first-frame' && m.current && m.trusted)", 'hidden prewarm first-frame');
+ const warmed = await state();
+ const prewarmRequests = await evaluate('__phase.requests.filter(r => r.path.endsWith("/desktop"))');
+ assert.equal(prewarmRequests.length, 1);
+ assert.deepEqual(prewarmRequests[0].body, { mode: 'view', replace_own_control: false, existing_only: true });
+ assert.equal(await evaluate("document.querySelector('#desktop iframe').hidden"), true);
+ assert.ok(group.runtimeDesktop.slice(desktopBefore).length > 0);
+ assert.ok(group.runtimeDesktop.slice(desktopBefore).every(row => row.prepare === false));
+ const retainedControl = await controlStatus();
+ assert.equal(retainedControl.status, 200);
+ assert.equal(retainedControl.body.view_only, false);
+ const prewarmed = await click('view');
+ verifyReuse(warmed, await state());
+ group.samples.push({ kind: stage, ...prewarmed, selectionPrewarmMs: (await evaluate("__phase.messages.find(m => m.type === 'first-frame' && m.current).at")) - prewarmRequests[0].at });
+ await capture('prewarmed');
+ await check('prewarm is existing_only, prepare=false and preserves existing control', { prewarmRequests, runtime: group.runtimeDesktop.slice(desktopBefore), retainedControl, before: warmed, after: await state() });
+
+ stage = 'explicit-control-handback';
+ await remember();
+ const beforeControl = await state();
+ await click('control');
+ await wait("document.querySelector('#desktop iframe').title.includes('接管')");
+ await wait("__phase.messages.some(m => m.mode === 'control' && m.type === 'first-frame' && m.current)");
+ const afterControl = await state();
+ assert.equal(afterControl.authorizations, beforeControl.authorizations + 1);
+ assert.equal(afterControl.connections, beforeControl.connections + 1);
+ assert.notEqual(afterControl.frameId, beforeControl.frameId);
+ await observeDisposal('Control replacement');
+ assert.equal((await controlStatus()).status, 401);
+ const controlRequest = await evaluate('__phase.requests.filter(r => r.path.endsWith("/desktop")).at(-1)');
+ assert.deepEqual(controlRequest.body, { mode: 'control', replace_own_control: true });
+ const currentMandatory = () => evaluate("fetch(new URL('mandatory.json', document.querySelector('#desktop iframe').contentWindow.location.href)).then(async r => ({ status: r.status, body: await r.json() }))");
+ assert.equal((await currentMandatory()).body.view_only, false);
+ await remember();
+ await click('handback');
+ await wait("__phase.messages.some(m => m.mode === 'view' && m.type === 'first-frame' && m.current && m.frameId === __phase.id(document.querySelector('#desktop iframe')))");
+ const released = await observeDisposal('Handback');
+ const afterHandback = await state();
+ assert.equal(afterHandback.authorizations, afterControl.authorizations + 1);
+ assert.equal(afterHandback.connections, afterControl.connections + 1);
+ assert.equal((await currentMandatory()).body.view_only, true);
+ const transitions = await evaluate('__phase.requests.filter(r => r.path.endsWith("/desktop") || r.path.endsWith("/handback")).slice(-3)');
+ assert.ok(transitions[1].path.endsWith('/handback'));
+ assert.equal(transitions[1].status, 200);
+ assert.ok(transitions[2].at >= transitions[1].deliveredAt, 'new View follows confirmed Handback');
+ await check('Control freshly authorizes; confirmed Handback releases then reconnects view', { beforeControl, afterControl, afterHandback, transitions, released });
+
+ stage = 'selection-cancels-prewarm';
+ await evaluate('__phase.gate = {}');
+ await select(second.id);
+ await wait('__phase.requests.some(r => r.held)', 'real prewarm response held for selection race');
+ await select(first.id);
+ await wait("__phase.requests.find(r => r.held).aborted === true", 'selection abort signal');
+ await evaluate('{ const gate = __phase.gate; __phase.gate = null; gate.release(); }');
+ await wait("__phase.messages.some(m => m.type === 'first-frame' && m.current && m.frameId === __phase.id(document.querySelector('#desktop iframe')))");
+ const held = await evaluate('__phase.requests.find(r => r.held)');
+ assert.equal(held.released, true);
+ const finalRequests = await evaluate('__phase.requests.filter(r => r.path.endsWith("/desktop"))');
+ assert.ok(finalRequests.at(-1).path.includes(first.id));
+ assert.equal(await evaluate("document.querySelectorAll('#desktop iframe').length"), 1);
+ await check('selection cancels in-flight prewarm and ignores its late response', { held, currentRequest: finalRequests.at(-1), iframeCount: 1 });
+ await remember();
+ await evaluate("document.querySelector('#disconnect').click()");
+ await observeDisposal('Disconnect');
+ assert.equal(await evaluate("document.querySelectorAll('#desktop iframe').length"), 0);
+ group.pass = true;
+ } catch (error) {
+ group.pass = false;
+ report.failures.push({ index, stage, kind: failureKind(error), name: error.name, message: error.message });
+ if (page) {
+ try {
+ group.diagnostic = await page.evaluate(`({ status: document.querySelector('#desktop-status')?.textContent, requests: window.__phase?.requests, messages: window.__phase?.messages, clicks: window.__phase?.clicks, errors: window.__phaseErrors, childErrors: document.querySelector('#desktop iframe')?.contentWindow?.__phaseErrors })`);
+ const file = output.replace(/\.json$/, '') + `-${index}-failure.png`;
+ await writeFile(file, await page.screenshot({ fullPage: true }), { mode: 0o600 });
+ group.artifacts.push(file);
+ } catch (diagnosticError) { group.diagnosticError = { name: diagnosticError.name, message: diagnosticError.message }; }
+ }
+ } finally {
+ group.rfb = { ...rfb?.counters };
+ group.fixtureCounters = { ...f?.counters };
+ group.cleanup = {};
+ for (const [name, cleanup] of [
+ ['browserClosed', async () => { if (browser) await browser.close(); return true; }],
+ ['fixture', async () => f ? f.close() : null],
+ ['rfb', async () => rfb ? rfb.close() : null],
+ ['profileRemoved', async () => { await rm(profile, { recursive: true, force: true }); await assert.rejects(access(profile), { code: 'ENOENT' }); return true; }],
+ ]) {
+ try { group.cleanup[name] = await cleanup(); }
+ catch (error) { group.pass = false; report.failures.push({ index, stage: 'cleanup:' + name, kind: 'cleanup', message: error.message }); }
+ }
+ await save();
+ }
+}
+
+async function main() {
+ const { values } = parseArgs({ options: { loader: { type: 'string' }, output: { type: 'string' }, 'source-root': { type: 'string', default: path.resolve(import.meta.dirname, '../../..') }, samples: { type: 'string', default: '5' }, 'browser-path': { type: 'string', default: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' } } });
+ assert.ok(values.loader && values.output, '--loader (installed omowright loader) and --output required');
+ const count = Number(values.samples);
+ assert.ok(Number.isSafeInteger(count) && count >= 1 && count <= 100, '--samples must be an integer between 1 and 100');
+ const output = path.resolve(values.output), root = path.resolve(values['source-root']);
+ const report = { schema: 1, at: new Date().toISOString(), invocation: [process.execPath, ...process.argv.slice(1)], node: process.version, requestedSamplesPerKind: count, groups: [], failures: [],
+ definitions: { 'no-prewarm-first-open': 'Fresh isolated Chromium profile and real API/fake-Codex fixture. Real View activates at first enabled microtask before prewarm; verified no existing_only request.', 'prewarmed-open': 'Same group browser after reload; wait for hidden genuine first-frame, then real View reveals the retained iframe.', 'repeat-open': 'Second explicit View on the first connected, unexpired read-only iframe.', firstPixelMs: 'Real button capture handler to next requestAnimationFrame with visible correctly decoded canvas pixels, same page performance clock.', selectionPrewarmMs: 'Background prewarm request dispatch to real first-frame message; excluded from prewarmed click latency.', quantiles: 'Nearest-rank p50/p95 over successful samples only; counts and failed stages retained independently.' },
+ limits: ['Controlled Chromium, synthetic RFB pattern and fake Codex; no real credentials, models, Docker, live state or live leases.', 'In-page DOM button dispatch, not OS pointer latency. 100ms synthetic frame delay exercises handshake-versus-pixel distinction.', 'A held real HTTP response is used only for cancellation; no product source, timers or viewer events are replaced.', 'Small sample p95 is descriptive, not a production latency claim.'] };
+ await writeEvidence(output, report);
+ const onUnhandled = error => { report.failures.push({ stage: 'unhandled-rejection', kind: failureKind(error), name: error?.name, message: String(error?.message ?? error) }); };
+ process.on('unhandledRejection', onUnhandled);
+ try {
+ report.source = await sourceWithViewer(root);
+ const { loadOmowright } = await import(pathToFileURL(path.resolve(values.loader)));
+ const { omowright } = await loadOmowright();
+ for (let index = 0; index < count; index++) await runGroup({ root, omowright, browserPath: values['browser-path'], output, index, report });
+ report.sourceAfter = await sourceWithViewer(root);
+ report.sourceStable = JSON.stringify(report.source) === JSON.stringify(report.sourceAfter);
+ if (!report.sourceStable) report.failures.push({ stage: 'source-stability', kind: 'environment', message: 'Sources or embedded bundle changed during run; rerun after workers settle.' });
+ } catch (error) { report.failures.push({ stage: 'setup', kind: failureKind(error), name: error.name, message: error.message }); }
+ report.summary = summarizeSamples(report.groups.flatMap(group => group.samples));
+ report.pass = report.failures.length === 0 && report.groups.length === count && report.groups.every(group => group.pass)
+ && kinds.every(kind => report.summary[kind].samples === count);
+ await writeEvidence(output, report);
+ process.off('unhandledRejection', onUnhandled);
+ console.log(JSON.stringify({ artifact: output, pass: report.pass, summary: report.summary, failures: report.failures }));
+ if (!report.pass) process.exitCode = 1;
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === import.meta.filename) {
+ if (process.env.NODE_AGENT_PHASE_ONE_CHILD === '1') await main();
+ else {
+ const home = await syntheticHome();
+ try {
+ process.exitCode = await new Promise((resolve, reject) => {
+ const child = fork(import.meta.filename, process.argv.slice(2), { env: { ...process.env, HOME: home, NODE_AGENT_PHASE_ONE_CHILD: '1' }, stdio: 'inherit' });
+ child.once('error', reject); child.once('exit', code => resolve(code ?? 1));
+ });
+ } finally { await rm(home, { recursive: true, force: true }); }
+ }
+}
diff --git a/tools/node-agent-api/build-viewer.mjs b/tools/node-agent-api/build-viewer.mjs
new file mode 100644
index 0000000..fad0311
--- /dev/null
+++ b/tools/node-agent-api/build-viewer.mjs
@@ -0,0 +1,31 @@
+import { build } from 'esbuild';
+import { createHash } from 'node:crypto';
+import { mkdir, readFile, writeFile, rename, realpath } from 'node:fs/promises';
+import path from 'node:path';
+import { parseArgs } from 'node:util';
+import { pathToFileURL } from 'node:url';
+
+export async function buildViewer({ novncRoot, outputDirectory = path.resolve(import.meta.dirname, '../../.lab/node-agent-viewer') }) {
+ const root = await realpath(novncRoot);
+ const metadata = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
+ if (typeof metadata.version !== 'string') throw new Error('Missing noVNC package version');
+ const source = await readFile(path.join(root, 'core/rfb.js'), 'utf8');
+ if (!source.includes('_framebufferUpdate()') || !source.includes('_handleRect()')) throw new Error('Unsupported noVNC first-frame observer seam');
+ const result = await build({ entryPoints: [path.join(import.meta.dirname, 'web/viewer.js')], bundle: true, minify: true, format: 'esm', platform: 'browser', target: 'es2022', write: false, metafile: true, legalComments: 'inline',
+ plugins: [{ name: 'local-novnc', setup(builder) { builder.onResolve({ filter: /^@novnc\/core\/rfb\.js$/ }, () => ({ path: path.join(root, 'core/rfb.js') })); } }],
+ });
+ const body = result.outputFiles[0].contents;
+ const manifest = { schema: 1, file: 'viewer-' + createHash('sha256').update(body).digest('hex') + '.js', novncVersion: metadata.version, bytes: body.length, inputFiles: Object.keys(result.metafile.inputs).length };
+ await mkdir(outputDirectory, { recursive: true, mode: 0o700 });
+ await writeFile(path.join(outputDirectory, manifest.file), body, { mode: 0o600 });
+ const pending = path.join(outputDirectory, 'manifest-' + process.pid + '.json');
+ await writeFile(pending, JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 });
+ await rename(pending, path.join(outputDirectory, 'manifest.json'));
+ return manifest;
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
+ const { values } = parseArgs({ options: { 'novnc-root': { type: 'string' }, output: { type: 'string' } } });
+ if (!values['novnc-root']) throw new Error('Usage: build-viewer.mjs --novnc-root DIRECTORY [--output DIRECTORY]');
+ console.log(JSON.stringify(await buildViewer({ novncRoot: values['novnc-root'], ...(values.output ? { outputDirectory: path.resolve(values.output) } : {}) })));
+}
diff --git a/tools/node-agent-api/cli.mjs b/tools/node-agent-api/cli.mjs
index a5cef52..646b870 100644
--- a/tools/node-agent-api/cli.mjs
+++ b/tools/node-agent-api/cli.mjs
@@ -6,6 +6,7 @@ import { createCodexAdapter } from './codex.mjs';
import { createHarness } from './runtime/harness.mjs';
import { recoverServiceLocks } from './persistence.mjs';
import { createSharedRuntime } from './shared-runtime.mjs';
+import { loadEmbeddedViewer } from './viewer.mjs';
const flags = process.argv.slice(2), options = {};
for (let i = 0; i < flags.length; i += 2) {
@@ -31,7 +32,7 @@ else if (backend === 'codex') {
const harness = createHarness({ runtime, namespace, stateRoot, model: options['--model'], baseUrl: options['--base-url'], binary: options['--codex-bin'] });
adapter = createCodexAdapter({ runtime, harness, roster, namespace, stateRoot, artifactRoot: path.join(stateDirectory, 'artifacts') });
} else throw new Error('Unknown backend');
-const service = await startNodeAgentApi({ adapter, stateDirectory, port, ...(backend === 'codex' ? { modelOptions: { defaultModel: options['--model'] }, tokenhubOptions: { baseUrl: options['--base-url'] } } : {}) });
+const service = await startNodeAgentApi({ adapter, stateDirectory, port, embeddedViewer: loadEmbeddedViewer(), ...(backend === 'codex' ? { modelOptions: { defaultModel: options['--model'] }, tokenhubOptions: { baseUrl: options['--base-url'] } } : {}) });
console.log(JSON.stringify({ service: 'Node Agent API', version: '0.3.0', backend, origin: service.origin, ui: service.origin + '/ui/', owner_key_file: service.keyFile, writes_enabled: adapter.writesEnabled }));
async function stop() { await service.close(); process.exit(0); }
process.once('SIGTERM', () => { void stop(); }); process.once('SIGINT', () => { void stop(); });
diff --git a/tools/node-agent-api/codex.mjs b/tools/node-agent-api/codex.mjs
index 84b5837..e94d96c 100644
--- a/tools/node-agent-api/codex.mjs
+++ b/tools/node-agent-api/codex.mjs
@@ -6,14 +6,33 @@ import path from 'node:path';
import { ApiError, identifier } from './errors.mjs';
import { resourceNames } from './runtime/common.mjs';
import { normalizeApplicationId } from './runtime/applications.mjs';
+import { sameExecutor } from './runtime/harness.mjs';
const execute = promisify(execFile);
const outcomes = { inProgress: 'running', completed: 'completed', failed: 'failed', interrupted: 'interrupted' };
export function createCodexAdapter({ runtime, harness, roster, namespace, stateRoot, registryFile, artifactRoot }) {
let store, unsubscribe, tail = Promise.resolve();
+ let deltaBuffer = [], deltaTimer;
const liveThreads = new Set();
+ const preparedExecutors = new Map();
const busyActions = new Set();
+ const validatesExecutor = !!runtime.shared && typeof runtime.validateExecutor === 'function' && typeof harness.connectionIdentity === 'function';
+ function invalidateExecutor(botId) {
+ preparedExecutors.delete(botId);
+ for (const session of store.list()) if (session.agent_id === botId) liveThreads.delete(session.thread_id);
+ }
+ async function prepareExecutor(botId) {
+ const prepared = await runtime.ensure(botId);
+ const identity = validatesExecutor ? await runtime.validateExecutor(botId) : null;
+ if (identity && !sameExecutor(prepared, identity)) throw new ApiError(409, 'execution_changed', 'Executor changed during preparation');
+ await harness.connect?.(botId, { prepared: true, executorIdentity: identity });
+ return identity;
+ }
+ function rememberExecutor(botId, identity) {
+ const connection = harness.connectionIdentity?.(botId);
+ if (identity && connection) preparedExecutors.set(botId, { identity: { ...identity }, connection });
+ }
let tokenhub;
async function threadModel(session) {
const selection = session.model ? { model: session.model } : {};
@@ -26,6 +45,21 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
}
function requireIdleBot(session) { if (store.list().some(row => (runtime.shared || row.agent_id === session.agent_id) && row.task_status === 'running')) throw new ApiError(409, 'turn_active', 'Interrupt active work before changing the shared project or environment'); }
function enqueue(action) { tail = tail.then(action); return tail; }
+ function flushDeltas() {
+ clearTimeout(deltaTimer); deltaTimer = undefined;
+ if (!deltaBuffer.length) return tail;
+ const batch = deltaBuffer; deltaBuffer = [];
+ return enqueue(async () => {
+ const operations = [];
+ for (const { botId, message } of batch) {
+ for (const session of store.list()) if (session.agent_id === botId && session.thread_id === message.params.threadId) {
+ operations.push({ id: session.id, events: [{ type: 'node.harness.event', data: { method: message.method, params: message.params } }] });
+ }
+ }
+ if (operations.length) await store.commitBatch(operations);
+ });
+ }
+ function drain() { flushDeltas(); return tail; }
async function syncRegistry() {
if (!registryFile) return;
const bots = await roster.agents(), environments = [];
@@ -41,22 +75,33 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
attachStore(value) {
store = value;
for (const session of store.list()) if (session.task_status === 'running') {
- for (const turn of session.turns ?? []) if (turn.status === 'running') turn.status = 'unknown';
- for (const action of session.actions ?? []) if (action.status === 'pending') action.status = 'unknown';
- void enqueue(async () => { await store.update(session.id, { turns: session.turns, actions: session.actions, task_status: 'unknown', active_turn_id: null }); await store.append(session.id, 'node.session.turn.unknown', { reason: 'service_restart', replayed: false }); });
+ const turns = structuredClone(session.turns ?? []), actions = structuredClone(session.actions ?? []);
+ for (const turn of turns) if (turn.status === 'running') turn.status = 'unknown';
+ for (const action of actions) if (action.status === 'pending') action.status = 'unknown';
+ void enqueue(() => store.commit(session.id, { changes: { turns, actions, task_status: 'unknown', active_turn_id: null }, events: [{ type: 'node.session.turn.unknown', data: { reason: 'service_restart', replayed: false } }] })).catch(() => {});
}
- unsubscribe = harness.subscribe(event => { void enqueue(async () => {
+ unsubscribe = harness.subscribe(event => {
+ const { message } = event;
+ if (message.id == null && typeof message.params?.delta === 'string' && /^item\/.*(?:\/delta|Delta)$/.test(message.method)) {
+ deltaBuffer.push(event);
+ if (deltaBuffer.length === 64) void flushDeltas().catch(() => {});
+ else if (!deltaTimer) deltaTimer = setTimeout(() => { void flushDeltas().catch(() => {}); }, 25);
+ return;
+ }
+ flushDeltas();
+ if (message.method === 'lab/status') invalidateExecutor(event.botId);
+ void enqueue(async () => {
const message = event.message, params = message.params ?? {};
const matching = store.list().filter(session => session.agent_id === event.botId && (session.thread_id === params.threadId || message.method === 'lab/status'));
for (const session of matching) {
const turns = structuredClone(session.turns ?? []), actions = structuredClone(session.actions ?? []);
+ const changes = {};
if (message.method === 'lab/status') {
liveThreads.delete(session.thread_id);
const wasRunning = session.task_status === 'running';
for (const turn of turns) if (turn.status === 'running') { turn.status = 'unknown'; turn.ended_at = Date.now(); }
for (const action of actions) if (action.status === 'pending') action.status = 'unknown';
- await store.update(session.id, { turns, actions, ...(wasRunning ? { task_status: 'unknown', active_turn_id: null } : {}) });
- await store.append(session.id, wasRunning ? 'node.session.turn.unknown' : 'node.harness.disconnected', { reason: 'harness_disconnected', replayed: false });
+ await store.commit(session.id, { changes: { turns, actions, ...(wasRunning ? { task_status: 'unknown', active_turn_id: null } : {}) }, events: [{ type: wasRunning ? 'node.session.turn.unknown' : 'node.harness.disconnected', data: { reason: 'harness_disconnected', replayed: false } }] });
continue;
}
if (params.turn?.id && ['turn/started', 'turn/completed'].includes(message.method)) {
@@ -66,44 +111,68 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
if (status === 'interrupted' && session.cancel_requested === turn.id) status = 'cancelled';
turn.status = status; turn.error = params.turn.error ?? null;
if (message.method === 'turn/completed') turn.ended_at = Date.now();
- await store.update(session.id, { turns, task_status: status, active_turn_id: status === 'running' ? turn.id : null });
+ Object.assign(changes, { turns, task_status: status, active_turn_id: status === 'running' ? turn.id : null });
}
if (message.method === 'thread/tokenUsage/updated') {
const turn = turns.find(turn => turn.id === params.turnId);
- if (turn) { turn.usage = { native: params.tokenUsage, cost: null, source: 'thread/tokenUsage/updated' }; await store.update(session.id, { turns }); }
+ if (turn) { turn.usage = { native: params.tokenUsage, cost: null, source: 'thread/tokenUsage/updated' }; changes.turns = turns; }
}
if (message.id != null && ['item/commandExecution/requestApproval', 'item/fileChange/requestApproval', 'item/tool/call'].includes(message.method)) {
actions.push({ object: 'node.required_action', id: 'act_' + randomUUID(), request_id: message.id, thread_id: params.threadId, turn_id: params.turnId, item_id: params.itemId, type: message.method, status: 'pending', details: params, created_at: Date.now() });
- await store.update(session.id, { actions });
+ changes.actions = actions;
}
if (params.item?.id) {
const turn = turns.find(turn => turn.id === params.turnId);
- if (turn) { const index = turn.items.findIndex(item => item.id === params.item.id); const item = { ...params.item, turn_id: params.turnId }; if (index < 0) turn.items.push(item); else turn.items[index] = item; await store.update(session.id, { turns }); }
+ if (turn) { const index = turn.items.findIndex(item => item.id === params.item.id); const item = { ...params.item, turn_id: params.turnId }; if (index < 0) turn.items.push(item); else turn.items[index] = item; changes.turns = turns; }
}
- await store.append(session.id, 'node.harness.event', { method: message.method, params });
+ await store.commit(session.id, { changes, events: [{ type: 'node.harness.event', data: { method: message.method, params } }] });
}
}).catch(() => { /* Persistence faults keep the tail rejected: subsequent operations fail closed. */ }); });
},
async bindSession(session) {
await roster.requireAgent(session.agent_id);
- await runtime.ensure(session.agent_id); await syncRegistry();
+ const identity = await prepareExecutor(session.agent_id); await syncRegistry();
const result = await harness.request(session.agent_id, 'thread/start', await threadModel(session));
if (!result.thread?.id) throw new ApiError(502, 'harness_schema', 'Harness did not return a thread');
if (session.model && result.model !== session.model) throw new ApiError(502, 'model_mismatch', 'Harness did not use the selected model');
if (!session.model && result.model) await store.update(session.id, { model: result.model, model_source: 'harness' });
- await store.update(session.id, { thread_id: result.thread.id, context: 'codex_harness', task_status: 'idle', turns: [], actions: [], ...(runtime.shared ? { executor_container_id: (await runtime.status(session.agent_id)).containerId } : {}) });
+ const containerId = identity?.containerId ?? (runtime.shared ? (await runtime.status(session.agent_id)).containerId : null);
+ await store.update(session.id, { thread_id: result.thread.id, context: 'codex_harness', task_status: 'idle', turns: [], actions: [], ...(containerId ? { executor_container_id: containerId } : {}) });
liveThreads.add(result.thread.id);
+ rememberExecutor(session.agent_id, identity);
},
async resume(session) {
if (!session.thread_id) throw new ApiError(409, 'thread_missing', 'Session has no Harness thread');
- if (runtime.shared) {
- await runtime.ensure(session.agent_id);
+ let identity, ensured = false;
+ if (validatesExecutor) {
+ const prepared = preparedExecutors.get(session.agent_id);
+ let current;
+ const connection = harness.connectionIdentity(session.agent_id);
+ try { current = await runtime.validateExecutor(session.agent_id); }
+ catch (error) {
+ // A cold recovery may find the bot desktop explicitly unavailable; preparing then is what the
+ // full recovery path always did. A prepared generation going away is an assignment change, so it
+ // still fails closed, as do stale-token, ownership and transport faults.
+ if (prepared || error?.code !== 'desktop_unavailable') { invalidateExecutor(session.agent_id); await harness.disconnect(session.agent_id); throw error; }
+ }
+ if (sameExecutor(prepared?.identity, current) && prepared?.connection === connection) {
+ if (liveThreads.has(session.thread_id)) return;
+ identity = current;
+ } else {
+ invalidateExecutor(session.agent_id);
+ if (connection) await harness.disconnect(session.agent_id);
+ identity = await prepareExecutor(session.agent_id);
+ }
+ ensured = true;
+ if (session.executor_container_id !== identity.containerId) await store.update(session.id, { executor_container_id: identity.containerId });
+ } else if (runtime.shared) {
+ await prepareExecutor(session.agent_id); ensured = true;
const current = await runtime.status(session.agent_id);
- if (session.executor_container_id && session.executor_container_id !== current.containerId) { await harness.disconnect(session.agent_id); liveThreads.delete(session.thread_id); }
- await store.update(session.id, { executor_container_id: current.containerId });
+ if (session.executor_container_id && session.executor_container_id !== current.containerId) { await harness.disconnect(session.agent_id); invalidateExecutor(session.agent_id); }
+ if (session.executor_container_id !== current.containerId) await store.update(session.id, { executor_container_id: current.containerId });
}
if (liveThreads.has(session.thread_id)) return;
- await runtime.ensure(session.agent_id);
+ if (!ensured) await prepareExecutor(session.agent_id);
try {
const resumed = await harness.request(session.agent_id, 'thread/resume', { threadId: session.thread_id, ...await threadModel(session) });
if (session.model && resumed.model !== session.model) throw new ApiError(502, 'model_mismatch', 'Harness did not resume the selected model');
@@ -117,17 +186,19 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
if (!session.model && empty.model) await store.update(session.id, { model: empty.model, model_source: 'harness' });
await store.update(session.id, { thread_id: empty.thread.id });
liveThreads.add(empty.thread.id);
+ rememberExecutor(session.agent_id, identity);
await store.append(session.id, 'node.thread.empty_recreated', { replayed: false }); return;
}
- liveThreads.add(session.thread_id);
const restored = await harness.request(session.agent_id, 'thread/read', { threadId: session.thread_id });
if (Array.isArray(restored.thread?.turns)) {
const turns = restored.thread.turns.map(turn => ({ object: 'node.turn', id: turn.id, ...(session.model ? { model: session.model } : {}), status: turn.status === 'interrupted' && session.cancel_requested === turn.id ? 'cancelled' : outcomes[turn.status] ?? 'unknown', items: (turn.items ?? []).map(item => ({ ...item, turn_id: turn.id })), usage: session.turns?.find(old => old.id === turn.id)?.usage ?? null, request_id: session.turns?.find(old => old.id === turn.id)?.request_id ?? null }));
await store.update(session.id, { turns, task_status: turns.at(-1)?.status ?? 'idle' });
}
+ liveThreads.add(session.thread_id);
+ rememberExecutor(session.agent_id, identity);
},
async inputSession(session, text, requestId) {
- await tail;
+ await drain();
if (session.task_status === 'running') throw new ApiError(409, 'turn_active', 'Wait for or cancel the active turn before submitting another');
await this.resume(session);
const result = await harness.request(session.agent_id, 'turn/start', { threadId: session.thread_id, input: [{ type: 'text', text }], ...(session.model ? { model: session.model } : {}), ...(session.reasoning_effort ? { reasoningEffort: session.reasoning_effort } : {}) });
@@ -142,9 +213,9 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
});
return { accepted: true, turn_id: result.turn.id };
},
- async itemsSession(session) { await tail; return (session.turns ?? []).flatMap(turn => turn.items); },
+ async itemsSession(session) { await drain(); return (session.turns ?? []).flatMap(turn => turn.items); },
async cancel(session, turnId) {
- await tail; identifier(turnId);
+ await drain(); identifier(turnId);
if (session.active_turn_id !== turnId || session.task_status !== 'running') throw new ApiError(409, 'turn_not_active', 'Turn does not belong to the active session');
await store.update(session.id, { cancel_requested: turnId });
const processes = [...new Set((session.turns?.find(turn => turn.id === turnId)?.items ?? []).filter(item => item.type === 'commandExecution' && item.status === 'inProgress' && /^[0-9]{1,10}$/.test(item.processId ?? '')).map(item => item.processId))];
@@ -157,10 +228,10 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
if (stopped.length) await store.append(session.id, 'node.turn.processes.stopped', { turn_id: turnId, processes: stopped });
return { accepted: true, turn_id: turnId, completed: false };
},
- async handoff(session) { requireIdleBot(session); await harness.request(session.agent_id, 'thread/unsubscribe', { threadId: session.thread_id }); if (runtime.shared) await harness.disconnect(session.agent_id); liveThreads.delete(session.thread_id); return { thread_id: session.thread_id, bot_id: session.agent_id, target: 'codex_cli', replayed: false }; },
- async actions(session) { await tail; return (session.actions ?? []).filter(action => action.status === 'pending'); },
+ async handoff(session) { requireIdleBot(session); invalidateExecutor(session.agent_id); await harness.request(session.agent_id, 'thread/unsubscribe', { threadId: session.thread_id }); if (runtime.shared) await harness.disconnect(session.agent_id); return { thread_id: session.thread_id, bot_id: session.agent_id, target: 'codex_cli', replayed: false }; },
+ async actions(session) { await drain(); return (session.actions ?? []).filter(action => action.status === 'pending'); },
async answer(session, actionId, decision) {
- await tail; const actions = structuredClone(session.actions ?? []), action = actions.find(action => action.id === actionId && action.status === 'pending');
+ await drain(); const actions = structuredClone(session.actions ?? []), action = actions.find(action => action.id === actionId && action.status === 'pending');
if (!action || !['accept', 'decline'].includes(decision)) throw new ApiError(409, 'action_unavailable', 'Approval is not pending or decision is invalid');
if (busyActions.has(actionId)) throw new ApiError(409, 'action_busy', 'Approval is already being answered');
busyActions.add(actionId);
@@ -173,15 +244,14 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
result = harness.redact(result);
await harness.answer(session.agent_id, action.request_id, { contentItems: [{ type: 'inputText', text: JSON.stringify(result) }], success: decision === 'accept' && result.exit_code === 0 });
} else await harness.answer(session.agent_id, action.request_id, { decision });
- action.status = decision; await store.update(session.id, { actions });
- await store.append(session.id, 'node.action.resolved', { action_id: actionId, decision });
+ action.status = decision; await store.commit(session.id, { changes: { actions }, events: [{ type: 'node.action.resolved', data: { action_id: actionId, decision } }] });
return { answered: true };
} finally { busyActions.delete(actionId); }
},
async applications(session) { return runtime.applications(session.agent_id); },
async launchApplication(session, kind) { if (!runtime.launchApplication) throw new ApiError(409, 'unsupported', 'Application launch is unavailable on this backend'); return runtime.launchApplication(session.agent_id, kind); },
- async desktop(id, target, mode = 'view') {
- if (runtime.shared) return runtime.desktop(id, target, mode);
+ async desktop(id, target, mode = 'view', options = { prepare: false }) {
+ if (runtime.shared) return runtime.desktop(id, target, mode, options);
await roster.requireAgent(id); const status = await runtime.status(id);
if (!status.running) throw new ApiError(409, 'desktop_unavailable', 'Bot environment is not running');
const descriptor = await runtime.descriptor(id);
@@ -201,6 +271,7 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
}
return { object: 'node.environment', id: 'nenv_' + id, agent_id: id, type: 'grok_node_codex', target: { type: target.type, application_id: applicationId }, isolation: 'per_bot_container', container: status.containerName, containerId: status.containerId, containerGenerationImmutableId: status.containerId, applicationId, display: 1, websocketUrl: `ws://127.0.0.1:${port}${path}`, websocketHeaders: { authorization: 'Bearer ' + authToken } };
},
+ ...(runtime.validateDesktop ? { validateDesktop: (id, environment) => runtime.validateDesktop(id, environment) } : {}),
async viewerAsset(asset, botId) {
if (!/^(vnc\.html|package\.json|(?:app|core|vendor)\/[A-Za-z0-9_./-]+)$/.test(asset) || asset.split('/').includes('..')) throw new ApiError(404, 'not_found', 'Asset unavailable');
if (runtime.shared) return runtime.viewerAsset(asset);
@@ -225,7 +296,7 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
},
async recreate(session) {
requireIdleBot(session);
- await harness.disconnect(session.agent_id); liveThreads.delete(session.thread_id);
+ invalidateExecutor(session.agent_id); await harness.disconnect(session.agent_id);
const result = await runtime.recreate(session.agent_id); await syncRegistry();
await store.append(session.id, 'node.environment.recreated', { processes_restored: false });
return { running: result.running, preserved_files: true, processes_restored: false };
@@ -234,7 +305,7 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
requireIdleBot(session);
identifier(input.backup_id);
const n = resourceNames(namespace, session.agent_id), backup = path.join(stateRoot, namespace, n.key, 'backups', input.backup_id);
- await harness.disconnect(session.agent_id); liveThreads.delete(session.thread_id);
+ invalidateExecutor(session.agent_id); await harness.disconnect(session.agent_id);
await runtime.ensure(session.agent_id); await runtime.restore(session.agent_id, backup);
await store.append(session.id, 'node.environment.restored', { backup_id: input.backup_id, processes_restored: false });
return { restored: true, processes_restored: false };
@@ -248,8 +319,8 @@ export function createCodexAdapter({ runtime, harness, roster, namespace, stateR
await writeFile(path.join(result.path, 'session.json'), JSON.stringify(session), { mode: 0o600 });
return { backup_id: path.basename(result.path), preserved: ['source', 'uncommitted_changes', 'conversation', 'logs'], processes_restored: false };
},
- async flush() { await tail; },
- async close() { unsubscribe?.(); await harness.close(); await tail; await runtime.close?.(); },
+ async flush() { await drain(); },
+ async close() { unsubscribe?.(); try { await drain(); } finally { preparedExecutors.clear(); liveThreads.clear(); await harness.close(); await runtime.close?.(); } },
};
return adapter;
}
diff --git a/tools/node-agent-api/desktop.mjs b/tools/node-agent-api/desktop.mjs
index 2248e77..981f10c 100644
--- a/tools/node-agent-api/desktop.mjs
+++ b/tools/node-agent-api/desktop.mjs
@@ -1,4 +1,4 @@
-import { randomBytes } from 'node:crypto';
+import { createHash, randomBytes } from 'node:crypto';
import { appendFile } from 'node:fs/promises';
import path from 'node:path';
import { WebSocket, WebSocketServer } from 'ws';
@@ -27,10 +27,12 @@ function sameEnvironment(left, right) {
return left?.display === right?.display
&& left?.websocketUrl === right?.websocketUrl
&& left?.containerGenerationImmutableId === right?.containerGenerationImmutableId
+ && left?.assignmentRevision === right?.assignmentRevision
+ && left?.containerStartedAt === right?.containerStartedAt
&& (left?.applicationId ?? null) === (right?.applicationId ?? null);
}
-export function createDesktop({ adapter, auth, store, governance, stateDirectory, clock = Date.now }) {
+export function createDesktop({ adapter, auth, store, governance, stateDirectory, embeddedViewer = null, clock = Date.now }) {
const tickets = new Map(), viewers = new Map(), leases = new Map(), connections = new Set();
const wss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
@@ -48,9 +50,7 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
function disconnect(viewer) {
if (leases.get(viewer.botId)?.id === viewer.id) leases.delete(viewer.botId);
for (const pair of [...connections]) if (pair.viewer === viewer) {
- connections.delete(pair);
- pair.client.terminate();
- pair.upstream.terminate();
+ pair.close();
}
}
@@ -68,16 +68,12 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
function selectViewer(req, pathname) {
const lane = /^\/desktop\/lane\/([A-Za-z0-9_-]+)(?:\/|$)/.exec(pathname);
const ids = viewerCookies(req);
- let id;
- if (lane) id = ids.find(value => value === lane[1]);
- else id = ids.find(value => viewers.get(value)?.target.type === 'desktop');
+ const id = lane && ids.find(value => value === lane[1]);
const viewer = viewers.get(id);
if (!viewer || viewer.expiresAt <= clock() || !auth.active(viewer.principal)) {
if (viewer) { disconnect(viewer); viewers.delete(viewer.id); }
throw new ApiError(401, 'viewer_expired', 'Desktop authorization expired');
}
- if (lane && (viewer.target.type !== 'application' || viewer.id !== lane[1])) throw forbidden();
- if (!lane && viewer.target.type !== 'desktop') throw forbidden();
return viewer;
}
@@ -89,9 +85,7 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
}
function socketPath(viewer) {
- return viewer.target.type === 'application'
- ? `desktop/lane/${viewer.id}/socket`
- : 'desktop/socket';
+ return `desktop/lane/${viewer.id}/socket`;
}
return {
@@ -99,6 +93,8 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
const mode = input.mode ?? 'view', ttl = input.ttl_seconds ?? 60;
if (!['view', 'control'].includes(mode) || !Number.isInteger(ttl) || ttl < 1 || ttl > 300) throw badRequest('Invalid desktop mode or ticket lifetime');
if (input.replace_own_control !== undefined && typeof input.replace_own_control !== 'boolean') throw badRequest('replace_own_control must be boolean');
+ if (input.existing_only !== undefined && typeof input.existing_only !== 'boolean') throw badRequest('existing_only must be boolean');
+ if (input.existing_only === true && mode !== 'view') throw badRequest('existing_only requires view mode');
auth.requireScope(principal, mode === 'control' ? 'desktop.control' : 'desktop.view', session.agent_id);
const target = normalizeTarget(input.target);
const held = leases.get(session.agent_id);
@@ -109,7 +105,7 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
const environment = await adapter.desktop(session.agent_id, {
type: target.type,
...(target.applicationId ? { application_id: target.applicationId } : {}),
- }, mode);
+ }, mode, { prepare: input.existing_only !== true });
if (!environment?.websocketUrl || (environment.applicationId ?? null) !== target.applicationId) throw new ApiError(502, 'desktop_upstream', 'Desktop upstream returned an invalid lane');
for (const [key, ticket] of tickets) if (ticket.expiresAt <= clock()) tickets.delete(key);
if (tickets.size >= 1000) throw new ApiError(429, 'desktop_capacity', 'Desktop ticket capacity reached');
@@ -127,8 +123,9 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
const current = await adapter.desktop(ticket.botId, {
type: ticket.target.type,
...(ticket.target.applicationId ? { application_id: ticket.target.applicationId } : {}),
- }, ticket.mode);
+ }, ticket.mode, { prepare: false });
if (!sameEnvironment(ticket.environment, current)) throw new ApiError(409, 'desktop_changed', 'Bot desktop was reassigned; request a new authorization');
+ if (ticket.expiresAt <= clock() || !auth.active(ticket.principal) || store.get(ticket.sessionId)?.status === 'closed') throw new ApiError(401, 'ticket_invalid', 'Desktop authorization expired');
const lease = leases.get(ticket.botId);
const replacing = lease && controls(lease) && ticket.replaces === lease.id && ticket.principal.id === lease.principal.id;
if (lease && controls(lease) && ((ticket.mode === 'control' && !replacing) || (ticket.replaces && !replacing))) throw new ApiError(409, 'desktop_busy', 'Desktop control changed; request a new authorization');
@@ -136,22 +133,22 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
const viewer = { ...ticket, environment: current, id: nonce(), expiresAt: clock() + 15 * 60 * 1000 };
viewers.set(viewer.id, viewer);
if (viewer.mode === 'control') leases.set(viewer.botId, viewer);
- if (viewer.target.type === 'application') {
- const location = `/desktop/lane/${viewer.id}/vnc.html`;
- res.writeHead(303, {
- 'Set-Cookie': `nodeviewer=${viewer.id}; HttpOnly; SameSite=Strict; Path=/desktop/lane/${viewer.id}; Max-Age=900`,
- Location: location,
- });
- } else {
- res.writeHead(303, {
- 'Set-Cookie': `nodeviewer=${viewer.id}; HttpOnly; SameSite=Strict; Path=/desktop; Max-Age=900`,
- Location: '/desktop/vnc.html',
- });
- }
+ const location = `/desktop/lane/${viewer.id}/vnc.html`;
+ res.writeHead(303, {
+ 'Set-Cookie': `nodeviewer=${viewer.id}; HttpOnly; SameSite=Strict; Path=/desktop/lane/${viewer.id}; Max-Age=900`,
+ Location: location,
+ });
res.end(); return;
}
const viewer = selectViewer(req, url.pathname);
+ if (url.pathname === `/desktop/lane/${viewer.id}/close` && req.method === 'POST') {
+ if (req.headers.origin !== url.origin) throw forbidden();
+ disconnect(viewer); viewers.delete(viewer.id);
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': `nodeviewer=; HttpOnly; SameSite=Strict; Path=/desktop/lane/${viewer.id}; Max-Age=0` });
+ res.end(JSON.stringify({ closed: true }));
+ return;
+ }
if (req.method !== 'GET') throw forbidden();
const lane = /^\/desktop\/lane\/[A-Za-z0-9_-]+(?:\/(.*))?$/.exec(url.pathname);
@@ -167,12 +164,21 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
}
if (asset === 'defaults.json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{}'); return; }
- const content = await adapter.viewerAsset(asset, viewer.botId);
+ const embedded = embeddedViewer && (asset === 'vnc.html'
+ ? { body: embeddedViewer.html({ mode: controls(viewer) ? 'control' : 'view', expiresAt: viewer.expiresAt, socketPath: '/' + socketPath(viewer) }), contentType: 'text/html; charset=utf-8', cacheControl: 'no-store' }
+ : await embeddedViewer.asset(asset));
+ if (embeddedViewer && !embedded) throw new ApiError(404, 'not_found', 'Viewer asset unavailable');
+ const content = embedded ? embedded.body : await adapter.viewerAsset(asset, viewer.botId);
+ selectViewer(req, url.pathname);
const extension = asset.split('.').at(-1);
- const type = { html: 'text/html; charset=utf-8', js: 'text/javascript', css: 'text/css', png: 'image/png', svg: 'image/svg+xml', ico: 'image/x-icon', json: 'application/json' }[extension];
+ const type = embedded?.contentType ?? { html: 'text/html; charset=utf-8', js: 'text/javascript', css: 'text/css', png: 'image/png', svg: 'image/svg+xml', ico: 'image/x-icon', json: 'application/json' }[extension];
if (!type) throw new ApiError(404, 'not_found', 'Asset type unavailable');
- res.writeHead(200, { 'Content-Type': type });
- res.end(asset === 'vnc.html' ? Buffer.from(content).toString().replace('', STYLE + '') : content);
+ const body = !embedded && asset === 'vnc.html' ? Buffer.from(content).toString().replace('', STYLE + '') : content;
+ const etag = '"' + createHash('sha256').update(body).digest('hex') + '"';
+ const headers = { 'Content-Type': type, 'Cache-Control': embedded?.cacheControl ?? 'private, no-cache', ETag: etag };
+ const matches = headers['Cache-Control'] !== 'no-store' && String(req.headers['if-none-match'] ?? '').split(',').some(value => value.trim() === '*' || value.trim().replace(/^W\//, '') === etag);
+ res.writeHead(matches ? 304 : 200, headers);
+ res.end(matches ? undefined : body);
},
async upgrade(req, socket, head, origin) {
@@ -181,24 +187,26 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
const requestUrl = new URL(req.url, origin);
if (req.headers.host !== new URL(origin).host || req.headers.origin !== origin) throw forbidden();
const laneSocket = /^\/desktop\/lane\/[A-Za-z0-9_-]+\/socket$/;
- if (requestUrl.pathname !== '/desktop/socket' && !laneSocket.test(requestUrl.pathname)) throw forbidden();
+ if (!laneSocket.test(requestUrl.pathname)) throw forbidden();
viewer = selectViewer(req, requestUrl.pathname);
const current = await adapter.desktop(viewer.botId, {
type: viewer.target.type,
...(viewer.target.applicationId ? { application_id: viewer.target.applicationId } : {}),
- }, viewer.mode);
+ }, viewer.mode, { prepare: false });
if (!sameEnvironment(viewer.environment, current)) throw new ApiError(409, 'desktop_changed', 'Desktop reassigned');
viewer.environment = current;
if (governance) release = await governance.acquireStream({ id: viewer.principal.user_id ?? viewer.principal.id });
+ selectViewer(req, requestUrl.pathname);
wss.handleUpgrade(req, socket, head, client => {
const upstream = new WebSocket(current.websocketUrl, ['binary'], { maxPayload: 4 * 1024 * 1024, headers: current.websocketHeaders ?? {} });
const pair = { client, upstream, viewer };
connections.add(pair);
const filter = createRfbFilter(() => controls(viewer));
- let waiting = [], waitingBytes = 0, closed = false;
+ let waiting = [], waitingBytes = 0, closed = false, validationTimer, validationDeadline;
const close = reason => {
if (closed) return;
closed = true;
+ clearTimeout(timer); clearTimeout(validationTimer); clearTimeout(validationDeadline);
release();
connections.delete(pair);
if (leases.get(viewer.botId)?.id === viewer.id) leases.delete(viewer.botId);
@@ -208,6 +216,28 @@ export function createDesktop({ adapter, auth, store, governance, stateDirectory
};
const ttlMs = Math.max(1, Math.min(viewer.expiresAt, viewer.principal.expiresAt) - clock());
const timer = setTimeout(() => close(new ApiError(408, 'viewer_expired', 'Viewer expired')), ttlMs); timer.unref();
+ pair.close = close;
+ if (adapter.validateDesktop) {
+ const validate = async () => {
+ if (closed) return;
+ validationDeadline = setTimeout(() => {
+ disconnect(viewer); viewers.delete(viewer.id);
+ close(new ApiError(408, 'desktop_validation_timeout', 'Desktop validation timed out'));
+ }, 5000);
+ validationDeadline.unref();
+ try {
+ const valid = await adapter.validateDesktop(viewer.botId, viewer.environment);
+ if (valid === false) throw new ApiError(409, 'desktop_changed', 'Desktop reassigned');
+ if (!closed) selectViewer(req, requestUrl.pathname);
+ } catch (error) {
+ disconnect(viewer); viewers.delete(viewer.id); close(error);
+ } finally {
+ clearTimeout(validationDeadline);
+ if (!closed) { validationTimer = setTimeout(validate, 1000); validationTimer.unref(); }
+ }
+ };
+ validationTimer = setTimeout(validate, 1000); validationTimer.unref();
+ }
upstream.on('open', () => { for (const message of waiting) upstream.send(message); waiting = []; waitingBytes = 0; });
client.on('message', bytes => {
try {
diff --git a/tools/node-agent-api/grok-node.mjs b/tools/node-agent-api/grok-node.mjs
index d12dd8b..d70d8b3 100644
--- a/tools/node-agent-api/grok-node.mjs
+++ b/tools/node-agent-api/grok-node.mjs
@@ -12,7 +12,50 @@ const token=g.token??g.authToken;
if(!token)throw new Error('Gateway authentication unavailable');
(async()=>{const method=process.argv[1],body=process.argv[2];const response=await fetch('http://127.0.0.1:1340/api/'+method,{method:'POST',headers:{authorization:'Bearer '+token,'content-type':'application/json'},body,signal:AbortSignal.timeout(20000)});if(!response.ok){console.log(JSON.stringify({node_api_upstream_error:response.status}));process.exitCode=1;return}console.log(JSON.stringify(await response.json()))})().catch(()=>{console.error('Gateway unavailable');process.exitCode=1});`;
-export function createGrokNodeAdapter({ container = 'grok-node-local-vm', allowWrites = false, execute = run } = {}) {
+export const snapshotFunctionProgram = String.raw`
+const fs=require('node:fs'),http=require('node:http'),{createHash}=require('node:crypto');
+function snapshotFile(file){
+ const fd=fs.openSync(file,fs.constants.O_RDONLY|fs.constants.O_NONBLOCK);
+ try{
+ if(!fs.fstatSync(fd).isFile())throw Error('Invalid snapshot file');
+ const buffer=Buffer.alloc(1048577);let size=0,count;
+ while(size0)size+=count;
+ if(size>1048576)throw Error('Snapshot file too large');
+ return buffer.subarray(0,size).toString('utf8');
+ }finally{fs.closeSync(fd)}
+}
+async function readSnapshot(id){
+ const g=JSON.parse(snapshotFile('/home/box/sand-data/gateway.json')),token=g.token??g.authToken;
+ if(!token)throw Error('Gateway unavailable');
+ const call=(method,body)=>new Promise((resolve,reject)=>{
+ const payload=JSON.stringify(body);
+ const req=http.request({host:'127.0.0.1',port:1340,path:'/api/'+method,method:'POST',headers:{authorization:'Bearer '+token,'content-type':'application/json','content-length':Buffer.byteLength(payload)},signal:AbortSignal.timeout(20000)},res=>{
+ const chunks=[];let size=0;
+ res.on('data',c=>{size+=c.length;if(size>8*1024*1024){req.destroy();reject(Error('Response too large'))}else chunks.push(c)});
+ res.on('end',()=>{try{if(res.statusCode<200||res.statusCode>=300)throw Error('Gateway error '+res.statusCode);resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))}catch(e){reject(e)}});
+ });
+ req.on('error',reject);
+ req.end(payload);
+ });
+ const agents=await call('listAgents',{});
+ if(!Array.isArray(agents))throw Error('Invalid roster');
+ if(!agents.some(a=>a.id===id))return {error:'not_found'}
+ const status=await call('getForeverBoxStatus',{id});
+ if(!status||typeof status.state!=='string')throw Error('Invalid status');
+ if(status.state!=='running'||!status.vncUrl)return {error:'desktop_unavailable'}
+ const url=new URL(status.vncUrl),embedded=url.searchParams.get('path')??'',query=embedded.indexOf('?'),display=query<0?'1':new URLSearchParams(embedded.slice(query+1)).get('token');
+ if(!['127.0.0.1','localhost'].includes(url.hostname)||url.protocol!=='http:'||!['6080','6081'].includes(url.port)||!display||!/^[1-9][0-9]{0,2}$/.test(display))throw Error('Invalid endpoint');
+ const s=JSON.parse(snapshotFile('/home/box/.sand-window-assignments.json'));
+ if(s.assignments?.[id]!==Number(display))throw Error('Stale assignment');
+ const assigned=s.tokens?.[id];
+ if(assigned!=null){if(typeof assigned!=='string'||!assigned||snapshotFile('/tmp/sand-window-tokens.d/'+display).trim()!==assigned)throw Error('Stale token')}
+ const assignmentRevision=createHash('sha256').update(JSON.stringify([id,Number(display),assigned??null])).digest('hex');
+ return {status:{state:status.state,vncUrl:status.vncUrl},assignmentRevision};
+}`;
+export const snapshotProgram = snapshotFunctionProgram + String.raw`
+readSnapshot(process.argv[1]).then(result=>console.log(JSON.stringify(result))).catch(()=>{console.error('Desktop validation unavailable');process.exitCode=1});`;
+
+export function createGrokNodeAdapter({ container = 'grok-node-local-vm', allowWrites = false, execute = run, docker = 'docker', readSnapshot } = {}) {
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(container)) throw new Error('Invalid container name');
if (allowWrites && !container.startsWith('grok-node-lab-')) throw new Error('Writes require an experimental GrokNode container');
async function writeGuard(botId) {
@@ -24,10 +67,10 @@ export function createGrokNodeAdapter({ container = 'grok-node-local-vm', allowW
try { resourceNames(namespace, 'validate'); } catch { throw new ApiError(403, 'unsafe_backend', 'Experimental namespace label required'); }
if (info.Name !== '/' + container || !container.startsWith(namespace + '-') || !Array.isArray(info.Mounts) || info.Mounts.some(mount => mount.Type === 'bind' || mount.Destination === '/root/.codex') || (botId && labels[BOT] && labels[BOT] !== botId)) throw new ApiError(403, 'unsafe_backend', 'Container ownership or mount isolation check failed');
}
- async function call(method, args = {}) {
+ async function call(method, args = {}, containerId = container) {
if (!METHODS.has(method)) throw new Error('Unsupported gateway method');
try {
- const result = await execute('docker', ['exec', container, 'node', '-e', gatewayProgram, '--', method, JSON.stringify(args)], { timeout: 25000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
+ const result = await execute(docker, ['exec', containerId, 'node', '-e', gatewayProgram, '--', method, JSON.stringify(args)], { timeout: 25000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
return JSON.parse(result.stdout);
} catch { throw new ApiError(502, 'upstream_unavailable', 'GrokNode gateway unavailable; outcome may be unknown'); }
}
@@ -40,6 +83,9 @@ export function createGrokNodeAdapter({ container = 'grok-node-local-vm', allowW
async function desktop(id) {
await requireAgent(id);
const status = await call('getForeverBoxStatus', { id });
+ return desktopEnvironment(id, status);
+ }
+ function desktopEnvironment(id, status) {
if (status.state !== 'running' || !status.vncUrl) throw new ApiError(409, 'desktop_unavailable', 'Bot desktop is not running');
const url = new URL(status.vncUrl);
if (!['127.0.0.1', 'localhost'].includes(url.hostname) || url.protocol !== 'http:' || !['6080','6081'].includes(url.port)) throw new ApiError(502, 'upstream_schema', 'Unsupported desktop endpoint');
@@ -51,6 +97,16 @@ export function createGrokNodeAdapter({ container = 'grok-node-local-vm', allowW
}
return {
agents, requireAgent, desktop, writesEnabled: allowWrites,
+ async snapshot(id, containerId, generation) {
+ identifier(id);
+ let result;
+ try { result = readSnapshot ? await readSnapshot(id, generation) : JSON.parse((await execute(docker, ['exec', containerId, 'setpriv', '--reuid=box', '--regid=box', '--init-groups', '--bounding-set=-all', '--no-new-privs', '/usr/bin/env', '-i', 'PATH=/usr/local/bin:/usr/bin:/bin', 'node', '-e', snapshotProgram, '--', id], { timeout: 45000, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 })).stdout); }
+ catch { throw new ApiError(502, 'upstream_unavailable', 'Desktop validation unavailable'); }
+ if (result.error === 'not_found') throw new ApiError(404, 'not_found', 'Bot not found');
+ if (result.error === 'desktop_unavailable') throw new ApiError(409, 'desktop_unavailable', 'Bot desktop is not running');
+ if (!/^[a-f0-9]{64}$/.test(result.assignmentRevision)) throw new ApiError(502, 'upstream_schema', 'Invalid desktop revision');
+ return { ...desktopEnvironment(id, result.status), assignmentRevision: result.assignmentRevision };
+ },
// Internal bridge to existing authenticated GrokNode APIs. REST routes
// remain explicit; callers cannot select arbitrary gateway methods.
gateway: call,
diff --git a/tools/node-agent-api/runtime/codex.mjs b/tools/node-agent-api/runtime/codex.mjs
index 66c140c..834b91f 100644
--- a/tools/node-agent-api/runtime/codex.mjs
+++ b/tools/node-agent-api/runtime/codex.mjs
@@ -6,15 +6,15 @@ import { homedir } from 'node:os';
import { privateDirectory, resourceNames } from './common.mjs';
const execute = promisify(execFile);
-export async function prepareCodex({ namespace, botId, stateRoot, runtime, codexBinary = 'codex', authFile = join(homedir(), '.codex/auth.json'), model, baseUrl }) {
+export async function prepareCodex({ namespace, botId, stateRoot, runtime, codexBinary = 'codex', authFile = join(homedir(), '.codex/auth.json'), model, baseUrl, status, registry }) {
const n = resourceNames(namespace, botId);
const home = await privateDirectory(join(stateRoot, namespace, n.key, 'mac-codex'));
const env = { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: home, CODEX_HOME: home, TMPDIR: process.env.TMPDIR ?? '/tmp', TERM: process.env.TERM ?? 'xterm-256color', OCX_SHIM_BYPASS: '1', NO_PROXY: '127.0.0.1,localhost,::1', no_proxy: '127.0.0.1,localhost,::1' };
const version = await execute(codexBinary, ['--version'], { env, encoding: 'utf8' });
if (version.stdout.trim() !== 'codex-cli 0.160.0') throw new Error('Local Codex must be exactly 0.160.0');
- const status = await runtime.status(botId);
+ status ??= await runtime.status(botId);
if (!status.running) throw new Error('Bot environment unavailable');
- const registry = await runtime.descriptor(botId);
+ registry ??= await runtime.descriptor(botId);
const auth = resolve(authFile);
if (!(await lstat(auth)).isFile()) throw new Error('Mac auth file is missing');
const link = join(home, 'auth.json');
diff --git a/tools/node-agent-api/runtime/harness.mjs b/tools/node-agent-api/runtime/harness.mjs
index 7b267af..b7cd711 100644
--- a/tools/node-agent-api/runtime/harness.mjs
+++ b/tools/node-agent-api/runtime/harness.mjs
@@ -46,7 +46,11 @@ export function rpcParams(method, params = {}, workspaceRoot = '/workspace') {
return safe;
}
-export function createHarness({ runtime, namespace, stateRoot, model, baseUrl, binary = 'codex', spawnProcess = spawn, timeoutMs = 60000 }) {
+export function sameExecutor(left, right) {
+ return !!left && !!right && ['containerId', 'containerStartedAt', 'assignmentRevision', 'display'].every(key => left[key] != null && left[key] === right[key]);
+}
+
+export function createHarness({ runtime, namespace, stateRoot, model, baseUrl, authFile, binary = 'codex', spawnProcess = spawn, timeoutMs = 60000 }) {
const events = new EventEmitter();
const sessions = new Map();
const secrets = new Set();
@@ -59,17 +63,23 @@ export function createHarness({ runtime, namespace, stateRoot, model, baseUrl, b
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, /^(?:authorization|authToken|accessToken|apiKey|secret|bearerToken)$/i.test(k) ? '[redacted]' : redact(v)]));
return value;
}
- async function connect(botId) {
+ async function connect(botId, { prepared = false, executorIdentity } = {}) {
validateBotId(botId);
if (sessions.has(botId)) return sessions.get(botId);
- const pending = start(botId);
+ const pending = start(botId, prepared, executorIdentity);
sessions.set(botId, pending);
try { return await pending; } catch (error) { sessions.delete(botId); throw error; }
}
- async function start(botId) {
- await runtime.ensure(botId);
+ async function start(botId, prepared, executorIdentity) {
+ if (!prepared) await runtime.ensure(botId);
+ if (runtime.validateExecutor) {
+ const current = await runtime.validateExecutor(botId);
+ if (executorIdentity && !sameExecutor(executorIdentity, current)) throw new Error('Executor changed during Harness preparation');
+ executorIdentity = current;
+ }
const registry = await runtime.descriptor(botId), status = await runtime.status(botId);
const entry = registry.environments[0];
+ if (executorIdentity && (entry.containerId !== executorIdentity.containerId || entry.display !== executorIdentity.display || !sameExecutor(status, executorIdentity))) throw new Error('Executor changed during Harness preparation');
if (entry.transport === 'stdio') {
if (!isAbsolute(entry.program) || !Array.isArray(entry.args) || !entry.args.includes('exec-server') || !entry.args.includes('stdio') || !entry.args.includes('--no-new-privs')) throw new Error('Managed container stdio executor required');
} else {
@@ -77,7 +87,8 @@ export function createHarness({ runtime, namespace, stateRoot, model, baseUrl, b
if (url.protocol !== 'ws:' || url.hostname !== '127.0.0.1' || url.username || url.password) throw new Error('Owned loopback executor required');
secrets.add(entry.authToken);
}
- const command = await prepareCodex({ runtime, namespace, stateRoot, botId, codexBinary: binary, model, baseUrl });
+ const command = await prepareCodex({ runtime, namespace, stateRoot, botId, codexBinary: binary, model, baseUrl, authFile, registry, status });
+ if (executorIdentity && !sameExecutor(executorIdentity, await runtime.validateExecutor(botId))) throw new Error('Executor changed during Harness preparation');
if (!isAbsolute(command.cwd)) throw new Error('Private Harness home unavailable');
const child = spawnProcess(binary, ['app-server', '--strict-config', '-c', 'mcp_servers={}', '-c', 'features.apps=false', '-c', 'features.plugins=false'], { cwd: command.cwd, env: command.env, stdio: ['pipe','pipe','pipe'] });
const calls = new Map(), approvals = new Map();
@@ -126,7 +137,8 @@ export function createHarness({ runtime, namespace, stateRoot, model, baseUrl, b
return { request, approvals, send, child, codexHome: command.cwd };
}
return {
- redact,
+ redact, connect,
+ connectionIdentity(botId) { return sessions.get(botId) ?? null; },
subscribe(listener) { events.on('event', listener); return () => events.off('event', listener); },
async request(botId, method, params) { if (params?.providerConnection?.token) secrets.add(params.providerConnection.token); const safe = rpcParams(method, params, runtime.workspaceRoot ?? '/workspace'); return (await connect(botId)).request(method, safe); },
async answer(botId, id, result) {
diff --git a/tools/node-agent-api/server.mjs b/tools/node-agent-api/server.mjs
index bcc5a4d..29539b1 100644
--- a/tools/node-agent-api/server.mjs
+++ b/tools/node-agent-api/server.mjs
@@ -26,14 +26,14 @@ export async function startNodeAgentApi(options) {
} catch (error) { await release(); throw error; }
}
-async function startUnlocked({ adapter, stateDirectory, port = 0, pollIntervalMs = 2000, governanceOptions = {}, modelOptions, tokenhubOptions = {} }) {
+async function startUnlocked({ adapter, stateDirectory, port = 0, pollIntervalMs = 2000, governanceOptions = {}, modelOptions, tokenhubOptions = {}, embeddedViewer = null }) {
const auth = await createAuth(stateDirectory), store = await createStore(stateDirectory);
const tokenhub = modelOptions && adapter.independentSessions ? await createTokenHub(stateDirectory, tokenhubOptions) : null;
adapter.setTokenHub?.(tokenhub);
const models = modelOptions && adapter.independentSessions ? await createModelPolicy(stateDirectory, { ...modelOptions, tokenhub }) : null;
const governance = await createGovernance(stateDirectory, governanceOptions);
let webhooks; try { webhooks = await createWebhookOutbox(stateDirectory); } catch (error) { await governance.close(); throw error; }
- const desktop = createDesktop({ adapter, auth, store, governance, stateDirectory });
+ const desktop = createDesktop({ adapter, auth, store, governance, stateDirectory, embeddedViewer });
store.setWriteGuard(async values => {
const groups = new Map();
for (const value of values) { const subject = value.quota_user_id ?? 'owner'; groups.set(subject, (groups.get(subject) ?? 0) + Buffer.byteLength(JSON.stringify(value))); }
@@ -222,7 +222,7 @@ async function startUnlocked({ adapter, stateDirectory, port = 0, pollIntervalMs
if (req.method === 'GET') return respond(res, 200, await adapter.clipboard(session));
if (req.method === 'POST') { if (session.status === 'closed') throw new ApiError(409, 'session_closed', 'Session is closed'); if (!desktop.hasControl(principal, session)) throw forbidden(); const input = object(await body(req), ['text']); return respond(res, 200, await adapter.clipboard(session, input.text)); }
}
- if (parts[4] === 'desktop' && req.method === 'POST') { if (session.status === 'closed') throw new ApiError(409, 'session_closed', 'Session attachment is closed'); return respond(res, 201, await desktop.issue(principal, session, object(await body(req), ['mode','ttl_seconds','target','replace_own_control']), origin)); }
+ if (parts[4] === 'desktop' && req.method === 'POST') { if (session.status === 'closed') throw new ApiError(409, 'session_closed', 'Session attachment is closed'); return respond(res, 201, await desktop.issue(principal, session, object(await body(req), ['mode','ttl_seconds','target','replace_own_control','existing_only']), origin)); }
if (parts[4] === 'handback' && req.method === 'POST') return respond(res, 200, desktop.handback(principal, session));
if (parts[4] === 'events' && req.method === 'GET') {
if (req.headers.accept?.includes('text/event-stream')) {
diff --git a/tools/node-agent-api/shared-runtime.mjs b/tools/node-agent-api/shared-runtime.mjs
index f8ad43c..e2472e2 100644
--- a/tools/node-agent-api/shared-runtime.mjs
+++ b/tools/node-agent-api/shared-runtime.mjs
@@ -12,37 +12,100 @@ import { projectProgram } from './runtime/project.mjs';
import { listApplications, normalizeApplicationId } from './runtime/applications.mjs';
import { applicationStreamProgram } from './application-stream.mjs';
import { clipboardProgram } from './clipboard-x11.mjs';
+import { createSnapshotBridge } from './snapshot-bridge.mjs';
const executeDefault = promisify(execFile);
const preparation = String.raw`const fs=require('fs'),path=require('path');const p=process.argv[1];for(const dir of ['/home/box/sand-data/node-agent-api','/home/box/sand-data/node-agent-api/executor-home',p]){let cur='/';for(const part of dir.split('/').filter(Boolean)){cur=path.join(cur,part);try{if(fs.lstatSync(cur).isSymbolicLink())throw Error('Unsafe directory')}catch(e){if(e.code!=='ENOENT')throw e;fs.mkdirSync(cur)}}}if(fs.existsSync('/home/box/sand-data/node-agent-api/executor-home/auth.json'))throw Error('Executor credentials must be absent');try{fs.readFileSync('/root/.codex/auth.json');throw Error('Mac credentials exposed')}catch(e){if(!['EACCES','ENOENT'].includes(e.code))throw e}console.log('executor_private_home_ready');`;
-
-export async function createSharedRuntime({ container = 'grok-node-local-vm', stateRoot, namespace = 'grok-node-lab-shared', workspaceRoot = '/workspace/codex-projects/node-agent-api', docker = '/usr/local/bin/docker', execute = executeDefault, spawnProcess = spawn } = {}) {
+export async function createSharedRuntime({ container = 'grok-node-local-vm', stateRoot, namespace = 'grok-node-lab-shared', workspaceRoot = '/workspace/codex-projects/node-agent-api', docker = '/usr/local/bin/docker', execute = executeDefault, spawnProcess = spawn, spawnSnapshot = execute === executeDefault ? spawnProcess : null } = {}) {
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(container) || !/^\/workspace\/[A-Za-z0-9_./-]+$/.test(workspaceRoot) || workspaceRoot.split('/').includes('..')) throw new Error('Invalid shared runtime configuration');
await privateDirectory(stateRoot);
- const bridge = createGrokNodeAdapter({ container, execute });
+ let snapshots;
+ const bridge = createGrokNodeAdapter({ container, execute, docker, ...(spawnSnapshot ? { readSnapshot: (id, row) => snapshots.read(id, row) } : {}) });
const codePath = '/home/box/sand-data/codex-vm/bin/codex', executorHome = '/home/box/sand-data/node-agent-api/executor-home';
const viewerToken = randomBytes(32).toString('hex'), children = new Set(), sockets = new Set(), clipboardOwners = new Map();
let closed = false;
- let assetGeneration;
- const viewerAssets = new Map();
+ const viewerAssets = new Map(), assetRequests = new Map(), desktopPreparations = new Map(), executorPreparations = new Map();
+ let assetBytes = 0, assetBatchScheduled = false;
async function info() {
- try { const row = JSON.parse((await execute(docker, ['inspect', container], { timeout: 10000, encoding: 'utf8' })).stdout)[0]; if (row.Name !== '/' + container || !row.State?.Running) throw new Error('Container unavailable'); return row; }
- catch { throw new ApiError(503, 'execution_unavailable', 'Open GrokNode and its existing box; the API will not start or replace it'); }
+ try {
+ if (closed) throw new Error('Runtime closed');
+ const row = JSON.parse((await execute(docker, ['inspect', container], { timeout: 10000, maxBuffer: 1024 * 1024, encoding: 'utf8' })).stdout)[0];
+ if (closed || row.Name !== '/' + container || !row.State?.Running || typeof row.Id !== 'string' || !row.Id || typeof row.State.StartedAt !== 'string' || !row.State.StartedAt) throw new Error('Container unavailable');
+ snapshots?.observe(row);
+ return row;
+ }
+ catch { snapshots?.observe(null); throw new ApiError(503, 'execution_unavailable', 'Open GrokNode and its existing box; the API will not start or replace it'); }
}
const lowPrivilege = (id, args, interactive = false) => ['exec', ...(interactive ? ['-i'] : []), id, 'setpriv', '--reuid=box', '--regid=box', '--init-groups', '--bounding-set=-all', '--no-new-privs', '/usr/bin/env', '-i', 'HOME=' + executorHome, 'CODEX_HOME=' + executorHome, 'PATH=/usr/local/bin:/usr/bin:/bin', ...args];
- async function box(args, options = {}) { const row = await info(); try { return await execute(docker, lowPrivilege(row.Id, args), { encoding: 'utf8', timeout: 30000, maxBuffer: 16 * 1024 * 1024, ...options }); } catch { throw new ApiError(502, 'execution_failed', 'Shared Linux operation failed'); } }
- async function seat(botId, ensure = false) {
- await bridge.requireAgent(botId);
- if (ensure) await bridge.gateway('ensureForeverBox', { id: botId });
- const environment = await bridge.desktop(botId);
- const probe = 'const fs=require("fs"),s=JSON.parse(fs.readFileSync("/home/box/.sand-window-assignments.json","utf8"));const [id,d]=process.argv.slice(1);if(s.assignments[id]!==Number(d))process.exit(2);if(s.tokens?.[id]&&fs.readFileSync("/tmp/sand-window-tokens.d/"+d,"utf8").trim()!==s.tokens[id])process.exit(3);console.log("owned");';
- await box(['node', '-e', probe, botId, String(environment.display)]);
- return environment;
- }
- async function applications(botId) {
- const environment = await seat(botId);
+ if (spawnSnapshot) snapshots = createSnapshotBridge({ docker, spawnSnapshot, argsFor: lowPrivilege });
+ async function boxAt(id, args, options = {}) { try { return await execute(docker, lowPrivilege(id, args), { encoding: 'utf8', timeout: 30000, maxBuffer: 16 * 1024 * 1024, ...options }); } catch { throw new ApiError(502, 'execution_failed', 'Shared Linux operation failed'); } }
+ async function box(args, options = {}) { return boxAt((await info()).Id, args, options); }
+ async function seat(botId, prepare = false) {
const row = await info();
- return listApplications({ display: ':' + environment.display, sanitize: true, execFileImpl: (file, args, options, callback) => { execFile(docker, lowPrivilege(row.Id, ['env', 'DISPLAY=:' + environment.display, 'LC_ALL=' + options.env.LC_ALL, file, ...args]), { encoding: 'utf8', timeout: 10000 }, callback); } });
+ let environment;
+ try { environment = await bridge.snapshot(botId, row.Id, row); }
+ catch (error) {
+ if (!prepare || error.code !== 'desktop_unavailable') throw error;
+ const key = JSON.stringify([row.Id, row.State.StartedAt, botId]);
+ if (!desktopPreparations.has(key)) {
+ const pending = (async () => {
+ try { return await bridge.snapshot(botId, row.Id, row); }
+ catch (current) { if (current.code !== 'desktop_unavailable') throw current; }
+ await bridge.gateway('ensureForeverBox', { id: botId }, row.Id);
+ return bridge.snapshot(botId, row.Id, row);
+ })();
+ desktopPreparations.set(key, pending);
+ pending.finally(() => { if (desktopPreparations.get(key) === pending) desktopPreparations.delete(key); }).catch(() => {});
+ }
+ environment = await desktopPreparations.get(key);
+ }
+ if (snapshots) {
+ const after = await info();
+ if (after.Id !== row.Id || after.State.StartedAt !== row.State.StartedAt) throw new ApiError(409, 'desktop_changed', 'Bot desktop generation changed');
+ }
+ if (closed) throw new ApiError(503, 'execution_unavailable', 'Shared runtime is closed');
+ return { ...environment, containerId: row.Id, containerStartedAt: row.State.StartedAt };
+ }
+ async function applications(botId, environment) {
+ environment ??= await seat(botId);
+ return listApplications({ display: ':' + environment.display, sanitize: true, execFileImpl: (file, args, options, callback) => {
+ boxAt(environment.containerId, ['env', 'DISPLAY=:' + environment.display, 'LC_ALL=' + options.env.LC_ALL, file, ...args], { timeout: 10000 }).then(result => callback(null, result.stdout, result.stderr), callback);
+ } });
+ }
+ function status(environment) { return { running: true, exists: true, containerName: container, containerId: environment.containerId, containerStartedAt: environment.containerStartedAt, assignmentRevision: environment.assignmentRevision, display: environment.display, workspaceRoot, transport: 'stdio' }; }
+ function trimAssets() {
+ while (viewerAssets.size > 128 || assetBytes > 16 * 1024 * 1024) {
+ const key = viewerAssets.keys().next().value;
+ assetBytes -= viewerAssets.get(key).size; viewerAssets.delete(key);
+ }
+ }
+ async function loadAssetBatch() {
+ const batch = new Map(assetRequests); assetRequests.clear(); assetBatchScheduled = false;
+ try {
+ const row = await info();
+ // Generation comes from the already-required inspect: container ID +
+ // StartedAt + image digest. noVNC bytes are package-owned and change only
+ // with the image/container lifecycle, so warm hits cost zero extra execs.
+ // Stopped containers reject in info() and never serve cached bytes.
+ const generation = JSON.stringify([row.Id, row.State.StartedAt, row.Image ?? row.Config?.Image ?? null]);
+ for (const [asset, request] of batch) {
+ const key = generation + '\n' + asset;
+ let cached = viewerAssets.get(key);
+ if (!cached) {
+ cached = { size: 0, pending: null };
+ const record = cached;
+ record.pending = execute(docker, ['exec', row.Id, 'cat', '/usr/share/novnc/' + asset], { encoding: null, timeout: 10000, maxBuffer: 4 * 1024 * 1024 }).then(result => {
+ const bytes = result.stdout;
+ if (!Buffer.isBuffer(bytes) || bytes.length > 4 * 1024 * 1024) throw new ApiError(404, 'not_found', 'Asset unavailable');
+ if (viewerAssets.get(key) === record) { record.size = bytes.length; assetBytes += record.size; trimAssets(); }
+ return bytes;
+ });
+ viewerAssets.set(key, record); trimAssets();
+ record.pending.catch(() => { if (viewerAssets.get(key) === record) { assetBytes -= record.size; viewerAssets.delete(key); } });
+ } else { viewerAssets.delete(key); viewerAssets.set(key, cached); }
+ cached.pending.then(request.resolve, request.reject);
+ }
+ } catch (error) { for (const request of batch.values()) request.reject(error); }
}
const wss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
const server = createServer((_req, res) => { res.writeHead(401); res.end(); });
@@ -52,11 +115,10 @@ export async function createSharedRuntime({ container = 'grok-node-local-vm', st
const parts = new URL(req.url, 'http://localhost').pathname.split('/');
if (parts.length !== 6 || parts[1] !== 'application' || !['view', 'control'].includes(parts[5])) throw new Error('Invalid lane');
const botId = decodeURIComponent(parts[2]), display = Number(parts[3]), id = normalizeApplicationId(parts[4]);
- const current = await seat(botId); if (!id || display !== current.display || !(await applications(botId)).some(app => app.application_id === id)) throw new Error('Stale lane');
- const row = await info();
+ const current = await seat(botId); if (!id || display !== current.display || !(await applications(botId, current)).some(app => app.application_id === id)) throw new Error('Stale lane');
wss.handleUpgrade(req, socket, head, client => {
sockets.add(client);
- const child = spawnProcess(docker, lowPrivilege(row.Id, ['node', '-e', applicationStreamProgram, '--', botId, String(display), id, parts[5]], true), { stdio: ['pipe', 'pipe', 'ignore'] }); children.add(child);
+ const child = spawnProcess(docker, lowPrivilege(current.containerId, ['node', '-e', applicationStreamProgram, '--', botId, String(display), id, parts[5]], true), { stdio: ['pipe', 'pipe', 'ignore'] }); children.add(child);
let ended = false;
const end = () => { if (ended) return; ended = true; sockets.delete(client); children.delete(child); child.stdin.end(); child.kill('SIGTERM'); client.terminate(); };
client.on('message', data => { if (child.stdin.writableLength > 1024 * 1024) end(); else child.stdin.write(data); });
@@ -69,16 +131,22 @@ export async function createSharedRuntime({ container = 'grok-node-local-vm', st
const origin = 'ws://127.0.0.1:' + server.address().port;
const runtime = {
shared: true, workspaceRoot, roster: bridge, applications,
+ async validateDesktop(botId, previousEnvironment) {
+ const current = await seat(botId);
+ if (previousEnvironment && (previousEnvironment.agent_id !== botId || ['containerId', 'containerStartedAt', 'assignmentRevision', 'display'].some(key => current[key] !== previousEnvironment[key]))) throw new ApiError(409, 'desktop_changed', 'Bot desktop generation changed');
+ if (previousEnvironment?.applicationId && !(await applications(botId, current)).some(app => app.application_id === previousEnvironment.applicationId)) throw new ApiError(404, 'application_not_found', 'Application is no longer on this bot desktop');
+ return current;
+ },
+ async validateExecutor(botId) { const environment = await seat(botId); return { containerId: environment.containerId, containerStartedAt: environment.containerStartedAt, assignmentRevision: environment.assignmentRevision, display: environment.display }; },
async viewerAsset(asset) {
if (!/^(vnc\.html|package\.json|(?:app|core|vendor)\/[A-Za-z0-9_./-]+)$/.test(asset) || asset.split('/').includes('..')) throw new ApiError(404, 'not_found', 'Asset unavailable');
- const row = await info();
- if (assetGeneration !== row.Id) { viewerAssets.clear(); assetGeneration = row.Id; }
- if (!viewerAssets.has(asset)) {
- const pending = execute(docker, ['exec', row.Id, 'cat', '/usr/share/novnc/' + asset], { encoding: null, timeout: 10000, maxBuffer: 4 * 1024 * 1024 }).then(result => result.stdout);
- viewerAssets.set(asset, pending);
- pending.catch(() => { if (viewerAssets.get(asset) === pending) viewerAssets.delete(asset); });
- }
- return viewerAssets.get(asset);
+ if (assetRequests.has(asset)) return assetRequests.get(asset).pending;
+ if (assetRequests.size >= 128) throw new ApiError(503, 'asset_capacity', 'Viewer asset batch is full');
+ const request = {};
+ request.pending = new Promise((resolve, reject) => { request.resolve = resolve; request.reject = reject; });
+ assetRequests.set(asset, request);
+ if (!assetBatchScheduled) { assetBatchScheduled = true; setImmediate(() => { void loadAssetBatch(); }); }
+ return request.pending;
},
async launchApplication(botId, kind) {
if (!['terminal', 'browser'].includes(kind)) throw new ApiError(400, 'application_kind', 'Choose terminal or browser');
@@ -86,24 +154,32 @@ export async function createSharedRuntime({ container = 'grok-node-local-vm', st
const program = 'const fs=require("fs"),{spawn}=require("child_process");const [id,d,kind,cwd,home]=process.argv.slice(1),s=JSON.parse(fs.readFileSync("/home/box/.sand-window-assignments.json","utf8"));if(s.assignments[id]!==Number(d))process.exit(2);const child=spawn(kind==="terminal"?"xfce4-terminal":"google-chrome",kind==="terminal"?["--disable-server","--working-directory",cwd,"--title","Node Agent Linux project"]:["--no-sandbox","--disable-dev-shm-usage","--no-first-run","--class=box-chrome","--user-data-dir="+home+"/chrome-"+d,"--new-window","about:blank"],{cwd,env:{...process.env,DISPLAY:":"+d},stdio:"ignore",detached:true});child.on("error",()=>process.exit(3));child.unref();console.log(JSON.stringify({launched:true,kind,display:Number(d)}));';
return JSON.parse((await box(['node', '-e', program, '--', botId, String(environment.display), kind, workspaceRoot, executorHome])).stdout);
},
- async status(botId) { const row = await info(), environment = await seat(botId); return { running: true, exists: true, containerName: container, containerId: row.Id, display: environment.display, workspaceRoot, transport: 'stdio' }; },
+ async status(botId) { return status(await seat(botId)); },
async ensure(botId) {
- await info(); await seat(botId, true);
- const version = await box([codePath, '--version']);
- if (version.stdout.trim() !== 'codex-cli 0.160.0') throw new ApiError(409, 'codex_version', 'Install the already-authorized Codex 0.160.0 execution package; automatic upgrades are disabled');
- await box(['node', '-e', preparation, workspaceRoot]);
- return this.status(botId);
+ const environment = await seat(botId, true);
+ const key = JSON.stringify([environment.containerId, environment.containerStartedAt, environment.assignmentRevision]);
+ if (executorPreparations.get(botId)?.key !== key) {
+ const pending = (async () => {
+ const version = await boxAt(environment.containerId, [codePath, '--version']);
+ if (version.stdout.trim() !== 'codex-cli 0.160.0') throw new ApiError(409, 'codex_version', 'Install the already-authorized Codex 0.160.0 execution package; automatic upgrades are disabled');
+ await boxAt(environment.containerId, ['node', '-e', preparation, workspaceRoot]);
+ })();
+ const record = { key, pending }; executorPreparations.set(botId, record);
+ pending.catch(() => { if (executorPreparations.get(botId) === record) executorPreparations.delete(botId); });
+ }
+ await executorPreparations.get(botId).pending;
+ return status(environment);
},
async descriptor(botId) {
- const row = await info(), environment = await seat(botId);
+ const environment = await seat(botId), row = { Id: environment.containerId };
const args = ['exec', '--workdir', workspaceRoot, '-i', row.Id, 'setpriv', '--reuid=box', '--regid=box', '--init-groups', '--bounding-set=-all', '--no-new-privs', '/usr/bin/env', '-i', 'HOME=' + executorHome, 'CODEX_HOME=' + executorHome, 'DISPLAY=:' + environment.display, 'PATH=/usr/local/bin:/usr/bin:/bin', codePath, 'exec-server', '--listen', 'stdio'];
return { version: 1, namespace, environments: [{ botId, containerName: container, containerId: row.Id, display: environment.display, workspaceRoot, program: docker, args, transport: 'stdio' }] };
},
- async desktop(botId, target = { type: 'desktop' }, mode = 'view') {
- const row = await info(), environment = await seat(botId, true); let applicationId = null;
- if (target.type === 'application') { applicationId = normalizeApplicationId(target.application_id); if (!applicationId || !(await applications(botId)).some(app => app.application_id === applicationId)) throw new ApiError(404, 'application_not_found', 'Application is no longer on this bot desktop'); }
+ async desktop(botId, target = { type: 'desktop' }, mode = 'view', options = { prepare: false }) {
+ const environment = await seat(botId, options.prepare === true); let applicationId = null;
+ if (target.type === 'application') { applicationId = normalizeApplicationId(target.application_id); if (!applicationId || !(await applications(botId, environment)).some(app => app.application_id === applicationId)) throw new ApiError(404, 'application_not_found', 'Application is no longer on this bot desktop'); }
const websocketUrl = applicationId ? `${origin}/application/${encodeURIComponent(botId)}/${environment.display}/${applicationId}/${mode}` : environment.websocketUrl;
- return { ...environment, type: 'grok_node_codex', target: { type: target.type, application_id: applicationId }, applicationId, containerGenerationImmutableId: row.Id, workspaceRoot, websocketUrl, ...(applicationId ? { websocketHeaders: { authorization: 'Bearer ' + viewerToken } } : {}) };
+ return { ...environment, type: 'grok_node_codex', target: { type: target.type, application_id: applicationId }, applicationId, containerGenerationImmutableId: environment.containerId, workspaceRoot, websocketUrl, ...(applicationId ? { websocketHeaders: { authorization: 'Bearer ' + viewerToken } } : {}) };
},
async project(botId, action, input = {}) { await bridge.requireAgent(botId); return JSON.parse((await box(['node', '-e', projectProgram, '--', action, JSON.stringify(input), workspaceRoot])).stdout); },
async approvedShell(botId, command) { await bridge.requireAgent(botId); if (typeof command !== 'string' || !command.trim() || command.length > 8192) throw new ApiError(400, 'invalid_command', 'Invalid command'); const program = 'const r=require("child_process").spawnSync("/bin/bash",["-lc",process.argv[1]],{cwd:process.argv[2],encoding:"utf8",timeout:15000,maxBuffer:1048576,env:{PATH:"/usr/local/bin:/usr/bin:/bin",HOME:process.argv[3],DISPLAY:process.argv[4]}});console.log(JSON.stringify({exit_code:r.status,stdout:r.stdout??"",stderr:r.stderr??"",timed_out:r.error?.code==="ETIMEDOUT"}));'; const row = await info(), environment = await seat(botId); const args = ['exec', row.Id, 'setpriv', '--reuid=box', '--regid=box', '--init-groups', '--bounding-set=-all', '--no-new-privs', 'node', '-e', program, '--', command, workspaceRoot, executorHome, ':' + environment.display]; try { return JSON.parse((await execute(docker, args, { encoding: 'utf8', timeout: 20000 })).stdout); } catch { throw new ApiError(502, 'execution_failed', 'Approved Linux command failed'); } },
@@ -143,7 +219,7 @@ export async function createSharedRuntime({ container = 'grok-node-local-vm', st
return { restored: true, processes_restored: false };
},
async recreate() { throw new ApiError(409, 'managed_by_grok_node', 'Box lifecycle belongs to GrokNode; the API preserves its original program and services'); },
- async close() { closed = true; for (const client of sockets) client.terminate(); for (const child of children) { child.stdin.end(); child.kill('SIGTERM'); } wss.close(); await new Promise(resolve => server.close(resolve)); },
+ async close() { closed = true; snapshots?.close(); for (const client of sockets) client.terminate(); for (const child of children) { child.stdin.end(); child.kill('SIGTERM'); } wss.close(); await new Promise(resolve => server.close(resolve)); },
};
return runtime;
}
diff --git a/tools/node-agent-api/snapshot-bridge.mjs b/tools/node-agent-api/snapshot-bridge.mjs
new file mode 100644
index 0000000..4c5f4ca
--- /dev/null
+++ b/tools/node-agent-api/snapshot-bridge.mjs
@@ -0,0 +1,96 @@
+import { ApiError, identifier } from './errors.mjs';
+import { snapshotFunctionProgram } from './grok-node.mjs';
+
+// The wire protocol has one operation: freshly validate one bot's desktop.
+export const snapshotBridgeProgram = snapshotFunctionProgram + String.raw`
+let input=Buffer.alloc(0),active=0;
+const fail=()=>process.exit(1);
+process.stdin.on('data',chunk=>{
+ if(input.length+chunk.length>16384)return fail();
+ input=Buffer.concat([input,chunk]);
+ let end;
+ while((end=input.indexOf(10))>=0){
+ const line=input.subarray(0,end);input=input.subarray(end+1);
+ let request;
+ try{
+ request=JSON.parse(line.toString('utf8'));
+ if(Object.keys(request).sort().join(',')!=='botId,id'||!Number.isSafeInteger(request.id)||request.id<1||typeof request.botId!=='string'||!/^[\p{L}\p{N}][\p{L}\p{N}_-]{0,127}$/u.test(request.botId)||active>=16)throw Error();
+ }catch{return fail()}
+ active++;
+ readSnapshot(request.botId).then(result=>({id:request.id,result}),()=>({id:request.id,error:true})).then(response=>{
+ active--;
+ const output=JSON.stringify(response)+'\n';
+ if(Buffer.byteLength(output)>1048576||process.stdout.writableLength>1048576)return fail();
+ process.stdout.write(output);
+ }).catch(fail);
+ }
+});
+process.stdin.on('end',()=>process.exit(0));
+process.stdin.on('error',fail);
+process.stdout.on('error',fail);
+`;
+
+const unavailable = () => new ApiError(502, 'snapshot_unavailable', 'Desktop validation unavailable');
+const generationKey = row => JSON.stringify([row.Id, row.State.StartedAt]);
+
+export function createSnapshotBridge({ docker, spawnSnapshot, argsFor, timeoutMs = 45000, maxPending = 16, maxBytes = 1048576 }) {
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 45000 || !Number.isInteger(maxPending) || maxPending < 1 || maxPending > 16 || !Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > 1048576) throw new Error('Invalid snapshot limits');
+ let current = null, closed = false, sequence = 0;
+ function stop(record) {
+ if (!record || record.ended) return;
+ record.ended = true;
+ if (current === record) current = null;
+ for (const pending of record.pending.values()) { clearTimeout(pending.timer); pending.reject(unavailable()); }
+ record.pending.clear();
+ record.child.stdin.destroy();
+ record.child.kill('SIGKILL');
+ }
+ function observe(row) {
+ if (current && (!row || current.key !== generationKey(row))) stop(current);
+ }
+ function start(row) {
+ const child = spawnSnapshot(docker, argsFor(row.Id, ['node', '-e', snapshotBridgeProgram], true), { stdio: ['pipe', 'pipe', 'ignore'] });
+ const record = { key: generationKey(row), child, pending: new Map(), input: Buffer.alloc(0), ended: false };
+ current = record;
+ child.on('error', () => stop(record));
+ child.on('exit', () => stop(record));
+ child.on('close', () => stop(record));
+ child.stdin.on('error', () => stop(record));
+ child.stdout.on('error', () => stop(record));
+ child.stdout.on('end', () => stop(record));
+ child.stdout.on('data', chunk => {
+ if (record.ended) return;
+ if (record.input.length + chunk.length > maxBytes) return stop(record);
+ record.input = Buffer.concat([record.input, chunk]);
+ let end;
+ while ((end = record.input.indexOf(10)) >= 0) {
+ const line = record.input.subarray(0, end); record.input = record.input.subarray(end + 1);
+ try {
+ const response = JSON.parse(line.toString('utf8')), pending = record.pending.get(response.id);
+ if (!pending || response.error || !response.result || typeof response.result !== 'object') return stop(record);
+ record.pending.delete(response.id); clearTimeout(pending.timer); pending.resolve(response.result);
+ } catch { return stop(record); }
+ }
+ });
+ return record;
+ }
+ return {
+ observe,
+ async read(botId, row) {
+ identifier(botId);
+ if (closed || !row?.Id || !row.State?.Running || !row.State.StartedAt) throw unavailable();
+ observe(row);
+ let record;
+ try { record = current ?? start(row); } catch { throw unavailable(); }
+ if (record.pending.size >= maxPending) throw new ApiError(503, 'snapshot_capacity', 'Desktop validation capacity reached');
+ const id = ++sequence, payload = JSON.stringify({ id, botId }) + '\n';
+ if (record.child.stdin.writableLength + Buffer.byteLength(payload) > 16384) { stop(record); throw unavailable(); }
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(() => stop(record), timeoutMs);
+ record.pending.set(id, { resolve, reject, timer });
+ try { record.child.stdin.write(payload); } catch { stop(record); }
+ });
+ },
+ close() { closed = true; stop(current); },
+ };
+}
diff --git a/tools/node-agent-api/store.mjs b/tools/node-agent-api/store.mjs
index ccf78c2..305a48b 100644
--- a/tools/node-agent-api/store.mjs
+++ b/tools/node-agent-api/store.mjs
@@ -2,7 +2,7 @@ import { randomUUID, createHash } from 'node:crypto';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';
import { privateDirectory } from './runtime/common.mjs';
-import { ApiError } from './errors.mjs';
+import { ApiError, badRequest, identifier } from './errors.mjs';
import { atomicJson, readPrivateJson } from './persistence.mjs';
export async function createStore(directory) {
@@ -35,19 +35,44 @@ export async function createStore(directory) {
try { await atomicJson(file, snapshot); } catch (error) { fault = error; throw error; }
}
function get(id) { const value = sessions.get(id); if (!value) throw new ApiError(404, 'not_found', 'Session not found'); return value; }
- async function append(id, type, payload) {
+ async function commitBatch(operations) {
+ if (!Array.isArray(operations)) throw badRequest('Commit operations must be an array');
+ const copy = structuredClone(operations);
+ for (const operation of copy) {
+ if (!operation || typeof operation !== 'object' || Array.isArray(operation)) throw badRequest('Invalid commit operation');
+ identifier(operation.id);
+ const { changes = {}, events = [] } = operation;
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes) || Object.hasOwn(changes, 'id') || Object.hasOwn(changes, 'events')) throw badRequest('Invalid session changes');
+ if (!Array.isArray(events)) throw badRequest('Commit events must be an array');
+ for (const event of events) {
+ if (!event || typeof event !== 'object' || Array.isArray(event) || typeof event.type !== 'string' || !event.type) throw badRequest('Invalid commit event');
+ }
+ }
return transaction(async () => {
- const value = get(id), event = { id: 'nevt_' + randomUUID(), type, session_id: id, created_at: Math.floor(Date.now() / 1000), data: payload };
- if (value.events.length >= 10000) throw new ApiError(409, 'event_limit', 'Session event limit reached');
- const next = { ...value, events: [...value.events, event] };
- await persist([...sessions.values()].map(row => row.id === id ? next : row));
- value.events = next.events; emitter.emit(id, event); emitter.emit('*', event); return event;
+ if (!copy.length) return [];
+ const candidates = new Map(), committed = [];
+ for (const { id, changes = {}, events = [] } of copy) {
+ const value = candidates.get(id) ?? get(id);
+ if (value.events.length + events.length > 10000) throw new ApiError(409, 'event_limit', 'Session event limit reached');
+ const added = events.map(({ type, data }) => ({ id: 'nevt_' + randomUUID(), type, session_id: id, created_at: Math.floor(Date.now() / 1000), data }));
+ candidates.set(id, { ...value, ...changes, events: [...value.events, ...added] });
+ committed.push(...added);
+ }
+ await persist([...sessions.values()].map(row => candidates.get(row.id) ?? row));
+ for (const [id, next] of candidates) Object.assign(get(id), next);
+ for (const event of committed) { emitter.emit(event.session_id, event); emitter.emit('*', event); }
+ return committed;
});
}
+ async function commit(id, patch) {
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw badRequest('Invalid session commit');
+ return commitBatch([{ id, changes: patch.changes, events: patch.events }]);
+ }
+ async function append(id, type, payload) { return (await commit(id, { events: [{ type, data: payload }] }))[0]; }
if (recovered) await persist([...sessions.values()]);
const refreshes = new Map();
return {
- get, list: () => [...sessions.values()], append,
+ get, list: () => [...sessions.values()], append, commit, commitBatch,
async create(agentId, metadata = {}, options = {}) {
return transaction(async () => {
// GrokNode has one transcript per bot. API sessions are explicit attachments.
@@ -76,7 +101,7 @@ export async function createStore(directory) {
},
subscribe(id, listener) { get(id); emitter.on(id, listener); return () => emitter.off(id, listener); },
subscribeAll(listener) { emitter.on('*', listener); return () => emitter.off('*', listener); },
- async update(id, changes) { const copy = structuredClone(changes); return transaction(async () => { const value = get(id), next = { ...value, ...copy }; await persist([...sessions.values()].map(row => row.id === id ? next : row)); Object.assign(value, copy); return value; }); },
+ async update(id, changes) { await commit(id, { changes }); return get(id); },
async flush() { await Promise.allSettled([...refreshes.values()]); await saving; if (fault) throw fault; },
snapshot() { return JSON.stringify([...sessions.values()]); },
setWriteGuard(guard) { writeGuard = guard; },
diff --git a/tools/node-agent-api/viewer.mjs b/tools/node-agent-api/viewer.mjs
new file mode 100644
index 0000000..5e4ac21
--- /dev/null
+++ b/tools/node-agent-api/viewer.mjs
@@ -0,0 +1,22 @@
+import { createHash } from 'node:crypto';
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+
+export function loadEmbeddedViewer({ directory = path.resolve(import.meta.dirname, '../../.lab/node-agent-viewer') } = {}) {
+ let manifest;
+ try { manifest = JSON.parse(readFileSync(path.join(directory, 'manifest.json'), 'utf8')); }
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
+ if (manifest.schema !== 1 || !/^viewer-[a-f0-9]{64}\.js$/.test(manifest.file) || typeof manifest.novncVersion !== 'string') throw new Error('Invalid embedded viewer manifest');
+ const body = readFileSync(path.join(directory, manifest.file));
+ if (body.length > 1024 * 1024 || 'viewer-' + createHash('sha256').update(body).digest('hex') + '.js' !== manifest.file) throw new Error('Embedded viewer checksum mismatch');
+ return {
+ version: manifest.novncVersion,
+ asset(asset) { return asset === manifest.file ? { body, contentType: 'text/javascript; charset=utf-8', cacheControl: 'private, max-age=31536000, immutable' } : null; },
+ html({ mode, expiresAt, socketPath }) {
+ const socket = '/' + socketPath.replace(/^\//, '');
+ if (!['view', 'control'].includes(mode) || !Number.isSafeInteger(expiresAt) || !/^\/desktop\/lane\/[A-Za-z0-9_-]+\/socket$/.test(socket)) throw new Error('Invalid embedded viewer configuration');
+ const config = JSON.stringify({ mode, expiresAt, socketPath: socket }).replaceAll('<', '\\u003c');
+ return '机器人桌面正在连接桌面…
';
+ },
+ };
+}
diff --git a/tools/node-agent-api/web/app.js b/tools/node-agent-api/web/app.js
index fe37f8a..761e3b3 100644
--- a/tools/node-agent-api/web/app.js
+++ b/tools/node-agent-api/web/app.js
@@ -1,7 +1,7 @@
-import { capabilities, createClient, desktopURL, eventView, projectDownload, readSSE, sessionPath } from './client.js';
+import { capabilities, createClient, createPaintQueue, createRefresh, desktopURL, eventView, projectDownload, readSSE, sessionPath } from './client.js';
const $ = id => document.getElementById(id);
-const text = (id, value) => { $(id).textContent = value; };
+const text = (id, value) => { if ($(id).textContent !== value) $(id).textContent = value; };
const pretty = value => JSON.stringify(value, null, 2);
const stateName = value => ({ idle: '待开始', pending: '提交中', accepted: '已接收', running: '正在执行', completed: '已完成', failed: '未完成', cancelled: '已取消', interrupted: '已中断', unknown: '状态待确认', closed: '已归档' }[value] ?? '状态待确认');
let client, flags = capabilities(), botId = '', session, turnId = '', epoch = 0;
@@ -11,9 +11,28 @@ let modelSettings;
let defaultModel = null;
let authPending = false;
let desktopChanging = false;
+let currentViewer, prewarmTimer, prewarmTask, desktopRequest;
let modelRows = [], tokenhubSettings;
const cursors = new Map(), liveItems = new Map(), busy = new Map();
const status = value => text('status', value);
+const dirtyItems = new Set(), logs = [];
+let logsDirty = false, logsTruncated = false;
+const logDetails = $('logs').closest('details');
+const paints = createPaintQueue(() => {
+ for (const item of dirtyItems) {
+ item.text += item.chunks.join(''); item.chunks.length = 0;
+ if (!item.node) item.node = message($('transcript'), item.label, item.text);
+ else item.node.textContent = item.text;
+ }
+ dirtyItems.clear();
+ if (logsDirty && logDetails.open) {
+ const output = logs.map(row => row.formatted ??= pretty(row.envelope)).join('\n');
+ text('logs', (logsTruncated || output.length > 90000 ? '较早事件已从显示区截断。\n' : '') + output.slice(-90000));
+ logsDirty = false;
+ }
+});
+logDetails.addEventListener('toggle', () => { if (logDetails.open && logsDirty) paints.schedule(); });
+document.addEventListener('visibilitychange', () => paints.flush());
function controls() {
const attached = Boolean(client && session), writable = attached && session.status !== 'closed';
@@ -38,9 +57,12 @@ function controls() {
'launch-terminal': writable && flags.applications && flags.control, 'launch-browser': writable && flags.applications && flags.control,
backup: writable && flags.export, 'backup-id': writable && flags.import, 'restore-backup': writable && flags.import && Boolean($('backup-id').value),
};
- for (const [id, allowed] of Object.entries(enabled)) $(id).disabled = !allowed || busy.has(id);
- $('connect').disabled = authPending;
- $('connect').textContent = authPending ? '连接中…' : '连接';
+ for (const [id, allowed] of Object.entries(enabled)) {
+ const disabled = !allowed || busy.has(id);
+ if ($(id).disabled !== disabled) $(id).disabled = disabled;
+ }
+ if ($('connect').disabled !== authPending) $('connect').disabled = authPending;
+ text('connect', authPending ? '连接中…' : '连接');
}
async function run(id, action) {
@@ -53,7 +75,10 @@ async function run(id, action) {
}
function bind(id, action) { $(id).addEventListener('click', () => run(id, action)); }
function resetSession() {
+ disposeDesktop();
++epoch; controller.abort(); controller = new AbortController(); streamController?.abort();
+ paints.reset(); dirtyItems.clear(); logs.length = 0; logsDirty = false; logsTruncated = false;
+ loadItems.reset(); loadActions.reset();
busy.clear();
session = undefined; turnId = ''; sending = false; liveItems.clear();
desktopChanging = false;
@@ -187,25 +212,37 @@ function message(parent, label, value) {
const body = document.createElement('pre'); body.textContent = value;
article.append(heading, body); parent.append(article); return body;
}
-async function loadItems() {
+const loadItems = createRefresh(async current => {
const generation = epoch;
const rows = await client.list(`${sessionPath(session.id)}/items`, controller.signal);
- if (generation !== epoch) return;
+ if (generation !== epoch || !current()) return;
const fragment = document.createDocumentFragment();
+ const retained = new Set();
const roles = { user: '用户', assistant: '助手', tool: '工具', system: '系统' };
- for (const item of rows) {
+ for (const row of rows) {
+ const live = liveItems.get(row.id);
+ if (live && !live.completed && row.status !== 'completed') {
+ if (live.node) fragment.append(live.node.parentElement);
+ retained.add(live);
+ continue;
+ }
+ const item = live?.completed ?? row;
const label = roles[item.role] ?? ({ userMessage: '你', agentMessage: '助手', commandExecution: '终端', fileChange: '文件修改', reasoning: '进度', dynamicToolCall: '审批操作' }[item.type]) ?? '记录';
const value = item.text ?? (item.type === 'userMessage' ? (item.content ?? []).map(part => part.text ?? '').join('\n') : item.type === 'commandExecution' ? [item.command, item.aggregatedOutput, item.exitCode === null || item.exitCode === undefined ? '' : '退出码:' + item.exitCode].filter(Boolean).join('\n') : pretty(item));
message(fragment, label, value);
+ if (live) { liveItems.delete(row.id); dirtyItems.delete(live); }
+ }
+ for (const live of liveItems.values()) {
+ if (live.node && !retained.has(live)) fragment.append(live.node.parentElement);
}
$('transcript').replaceChildren(fragment);
- if (!rows.length) text('transcript', '当前会话没有返回对话记录。');
-}
-async function loadActions() {
+ if (!rows.length && !liveItems.size) text('transcript', '当前会话没有返回对话记录。');
+});
+const loadActions = createRefresh(async current => {
if (!flags.actions) { text('approvals', '服务未声明审批能力。'); return; }
const generation = epoch, path = `${sessionPath(session.id)}/actions`;
const rows = await client.list(path, controller.signal);
- if (generation !== epoch) return;
+ if (generation !== epoch || !current()) return;
$('approvals').replaceChildren();
for (const action of rows) {
const box = document.createElement('div'); box.className = 'approval';
@@ -225,7 +262,7 @@ async function loadActions() {
$('approvals').append(box);
}
if (!rows.length) text('approvals', '没有待审批操作。');
-}
+});
async function selectSession(id) {
resetSession();
@@ -240,6 +277,7 @@ async function selectSession(id) {
status(`会话已恢复 · ${stateName(value.task_status ?? value.status)}`);
text('turn-status', turnId ? `活动回合:${turnId} · 结果未知` : '尚未观察到权威回合结果');
controls();
+ scheduleDesktopPrewarm();
await Promise.all([loadItems(), loadActions(), loadSessions()]);
if (generation === epoch) startStream();
} catch (error) { if (generation === epoch && error.name !== 'AbortError') status(error.message); }
@@ -251,8 +289,10 @@ function receive(event) {
if (envelope.session_id && envelope.session_id !== session.id) return;
if (event.id) cursors.set(session.id, event.id);
const view = eventView(event);
- text('logs', ($('logs').textContent === '尚未收到事件' ? '' : $('logs').textContent + '\n') + pretty(envelope));
- if ($('logs').textContent.length > 100000) text('logs', '较早事件已从显示区截断。\n' + $('logs').textContent.slice(-90000));
+ logs.push({ envelope });
+ if (logs.length > 100) { logs.shift(); logsTruncated = true; }
+ logsDirty = true;
+ const previousTurnId = turnId;
if (view.turnStatus) {
const labels = { running: '运行中', completed: '已完成', failed: '失败', cancelled: '已取消', interrupted: '已中断', unknown: '结果未知' };
text('turn-status', `${view.turnId ?? '未提供回合 ID'} · ${labels[view.turnStatus] ?? view.turnStatus}`);
@@ -260,20 +300,31 @@ function receive(event) {
else if (!view.turnId || view.turnId === turnId) turnId = '';
}
if (view.delta !== undefined && view.itemId) {
- let node = liveItems.get(view.itemId);
- if (!node) {
- node = message($('transcript'), `流式输出 · ${view.method || view.type} · ${view.itemId}`, '');
- liveItems.set(view.itemId, node);
+ let item = liveItems.get(view.itemId);
+ if (!item) {
+ item = { label: `流式输出 · ${view.method || view.type} · ${view.itemId}`, text: '', chunks: [], node: undefined };
+ liveItems.set(view.itemId, item);
}
- node.textContent += view.delta;
+ item.chunks.push(view.delta); dirtyItems.add(item);
+ }
+ if (view.method === 'item/completed' && view.params.item?.id) {
+ const item = liveItems.get(view.params.item.id);
+ if (item) item.completed = view.params.item;
}
if (/input\.(accepted|rejected|unknown)$/.test(view.type)) status(`输入状态:${view.type.split('.').at(-1)};请以回合事件判断结果。`);
if (view.type === 'node.upstream.unavailable') status('上游不可用,任务结果未知;不会自动重发输入。');
- if (view.type === 'node.session.closed') { session.status = 'closed'; $('desktop').replaceChildren(); }
- if (view.type.endsWith('items.changed')) void run('items-update', async () => { await loadItems(); liveItems.clear(); });
+ if (view.type === 'node.session.closed') { session.status = 'closed'; disposeDesktop(); }
+ if (view.type.endsWith('items.changed')) refreshFromEvent(loadItems);
if (view.method === 'turn/diff/updated' && typeof view.params.diff === 'string') text('diff', view.params.diff || '服务返回空差异。');
- if (/action|approval/i.test(view.type + view.method) && flags.actions) void run('actions-update', loadActions);
- controls();
+ if (/action|approval/i.test(view.type + view.method) && flags.actions) refreshFromEvent(loadActions);
+ if (previousTurnId !== turnId || view.type === 'node.session.closed') controls();
+ if ((view.turnStatus && view.turnStatus !== 'running') || view.type === 'node.session.closed') paints.flush();
+ else if (dirtyItems.size || logDetails.open) paints.schedule();
+}
+
+function refreshFromEvent(refresh) {
+ const generation = epoch;
+ void refresh().catch(error => { if (generation === epoch && error.name !== 'AbortError') status(error.message); });
}
function startStream() {
@@ -294,6 +345,8 @@ function startStream() {
if (generation === epoch && !stream.signal.aborted) text('stream-status', '事件流已断开;点击恢复。回合结果可能未知。');
} catch (error) {
if (generation === epoch && !stream.signal.aborted) text('stream-status', `事件流不可用:${error.message}。可点击恢复;不会重发消息。`);
+ } finally {
+ if (generation === epoch && !stream.signal.aborted) paints.flush();
}
})();
}
@@ -339,7 +392,7 @@ bind('attach', async () => {
const value = await post('/v1/agents/sessions', { agent_id: botId, ...(modelSelection && $('model').value ? { model: $('model').value, reasoning_effort: $('reasoning-effort').value || null } : {}) });
await selectSession(value.id);
});
-bind('resume', async () => { await Promise.all([loadItems(), loadActions()]); liveItems.clear(); startStream(); });
+bind('resume', async () => { await Promise.all([loadItems(), loadActions()]); startStream(); });
bind('actions-refresh', loadActions);
$('composer').addEventListener('submit', event => {
event.preventDefault();
@@ -365,51 +418,176 @@ bind('cancel', async () => {
const value = await post(`${sessionPath(session.id)}/events`, { events: [{ type: 'agent.session.input.cancel', turn_id: turnId }] });
status(`取消请求响应:${pretty(value)};等待权威回合结果。`);
});
-async function desktop(mode, handback = false) {
- if (desktopChanging) return;
- desktopChanging = true; controls();
- let released = false;
- const generation = epoch, selectedSession = session.id;
- text('desktop-status', handback ? '正在交还控制并切回观看…' : mode === 'control' ? '正在打开接管窗口…' : '正在切换到观看…');
+function disposeDesktop() {
+ clearTimeout(prewarmTimer); prewarmTimer = undefined;
+ prewarmTask = undefined;
+ desktopRequest?.abort(); desktopRequest = undefined;
+ disposeCurrentViewer();
+}
+
+function disposeCurrentViewer() {
+ const viewer = currentViewer;
+ currentViewer = undefined;
+ if (!viewer) return;
+ clearTimeout(viewer.expiryTimer);
+ viewer.cancelLoad?.();
+ viewer.iframe.contentWindow?.postMessage({ source: 'node-agent-parent', type: 'dispose' }, location.origin);
+ // Removal can destroy the child before its queued dispose handler runs.
+ // Close its cookie-scoped lane here too; iframe removal disconnects its RFB.
try {
- if (handback) {
- await post(`${sessionPath(selectedSession)}/handback`, {});
- released = true;
+ const url = new URL(viewer.iframe.contentWindow.location.href);
+ if (url.origin === location.origin && /^\/desktop\/lane\/[^/]+\/vnc\.html$/.test(url.pathname)) {
+ void fetch(new URL('close', url), { method: 'POST', credentials: 'same-origin', keepalive: true }).catch(() => {});
+ }
+ } catch { /* A navigating document may not yet expose its lane. */ }
+ viewer.iframe.remove();
+}
+
+function scheduleDesktopPrewarm({ enabled = true, delay = 400 } = {}) {
+ if (!enabled || !client || !session || session.status === 'closed' || !flags.view) return;
+ clearTimeout(prewarmTimer);
+ const generation = epoch;
+ prewarmTimer = setTimeout(() => {
+ prewarmTimer = undefined;
+ if (generation !== epoch || desktopChanging) return;
+ const task = desktop('view', false, true);
+ prewarmTask = task;
+ void task.finally(() => { if (prewarmTask === task) prewarmTask = undefined; });
+ }, delay);
+}
+
+function reusableViewer() {
+ return currentViewer?.sessionId === session?.id && currentViewer.epoch === epoch
+ && currentViewer.mode === 'view' && currentViewer.connected && currentViewer.firstFrame
+ && currentViewer.expiresAt > Date.now();
+}
+
+window.addEventListener('message', event => {
+ const viewer = currentViewer, data = event.data;
+ if (!viewer || event.origin !== location.origin || event.source !== viewer.iframe.contentWindow
+ || viewer.epoch !== epoch || viewer.sessionId !== session?.id
+ || data?.source !== 'node-agent-viewer' || data.mode !== viewer.mode) return;
+ if (data.type === 'disconnected') {
+ const visible = !viewer.prewarm;
+ disposeCurrentViewer();
+ if (visible) text('desktop-status', '桌面连接已断开,请重新点击观看或接管。');
+ return;
}
- let value;
- try { value = await post(`${sessionPath(selectedSession)}/desktop`, { mode, replace_own_control: true }); }
- catch (error) {
- if (error.code === 'desktop_busy') { const message = '桌面已有控制窗口,或控制权刚刚发生变化。同一密钥可重新点击接管;其他使用者需要先交还控制。'; text('desktop-status', message); throw new Error(message); }
- throw error;
+ if (data.type === 'ready') {
+ if (!Number.isFinite(data.expiresAt)) return;
+ viewer.expiresAt = data.expiresAt;
+ viewer.checkedAt = Date.now();
+ viewer.connected = false; viewer.firstFrame = false;
+ clearTimeout(viewer.expiryTimer);
+ viewer.expiryTimer = setTimeout(() => {
+ if (currentViewer !== viewer) return;
+ const visible = !viewer.prewarm;
+ disposeCurrentViewer();
+ if (visible) text('desktop-status', '桌面授权已过期,请重新点击观看或接管。');
+ }, Math.max(0, Math.min(2147483647, viewer.expiresAt - Date.now())));
+ } else if (data.type === 'connected' && viewer.expiresAt > Date.now()) viewer.connected = true;
+ else if (data.type === 'first-frame' && viewer.connected && viewer.expiresAt > Date.now()) {
+ viewer.firstFrame = true;
+ if (!viewer.prewarm) text('desktop-status', viewer.mode === 'view' ? '只读画面已就绪。' : '接管画面已就绪。');
}
- const iframe = document.createElement('iframe');
- iframe.title = mode === 'control' ? '机器人桌面 · 接管' : '机器人桌面 · 观看';
- iframe.referrerPolicy = 'no-referrer'; iframe.src = desktopURL(value.url, location.origin);
- await new Promise((resolve, reject) => {
- const timer = setTimeout(() => reject(new Error('桌面查看器加载超时,请重新点击观看或接管。')), 30000);
- iframe.addEventListener('load', () => {
- clearTimeout(timer);
- try {
- const body = iframe.contentDocument?.body?.textContent ?? '';
- if (body.trim().startsWith('{')) {
- const response = JSON.parse(body);
- if (response.error) { reject(new Error('桌面授权已变化,请重新点击接管;其他使用者持有控制时需要先交还。')); return; }
- }
- resolve();
- } catch (error) { reject(error); }
- }, { once: true });
- $('desktop').replaceChildren(iframe);
- });
- if (generation === epoch) {
- text('desktop-status', handback ? '控制已交还,当前为只读观看;画面连接由查看器显示。' : `${mode === 'control' ? '已签发接管授权' : '已签发观看授权'};画面连接由查看器显示。${value.pauses_agent === false ? '接管不会暂停机器人。' : '机器人暂停状态未确认。'}`);
- if (handback) status('控制已交还,已切回观看。');
+});
+
+async function desktop(mode, handback = false, prewarm = false) {
+ if (!session || session.status === 'closed' || desktopChanging) return;
+ clearTimeout(prewarmTimer); prewarmTimer = undefined;
+ if (!handback && !prewarm && mode === 'view' && prewarmTask) {
+ const generation = epoch, task = prewarmTask;
+ desktopChanging = true; controls();
+ await task;
+ if (generation !== epoch) return;
+ if (prewarmTask === task) prewarmTask = undefined;
+ desktopChanging = false; controls();
+ }
+ if (!handback && !prewarm && mode === 'view' && reusableViewer() && Date.now() - currentViewer.checkedAt >= 30000) {
+ const viewer = currentViewer, generation = epoch;
+ viewer.iframe.hidden = true;
+ desktopChanging = true; controls();
+ try {
+ const url = new URL(viewer.iframe.contentWindow.location.href);
+ if (url.origin !== location.origin || !/^\/desktop\/lane\/[^/]+\/vnc\.html$/.test(url.pathname)) throw new Error('Viewer lane unavailable');
+ // The backend watchdog removes lanes whose desktop generation changes.
+ const response = await fetch(new URL('mandatory.json', url), {
+ credentials: 'same-origin', cache: 'no-store', signal: AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]),
+ });
+ if (!response.ok || (await response.json()).view_only !== true) throw new Error('Viewer lane expired');
+ viewer.checkedAt = Date.now();
+ } catch {
+ if (currentViewer === viewer) disposeCurrentViewer();
+ } finally {
+ if (generation === epoch) { desktopChanging = false; controls(); }
+ }
+ if (generation !== epoch) return;
}
+ if (!handback && !prewarm && mode === 'view' && reusableViewer()) {
+ currentViewer.prewarm = false; currentViewer.iframe.hidden = false;
+ text('desktop-status', '只读画面已就绪。');
+ return;
+ }
+ const generation = epoch, selectedSession = session.id;
+ const request = new AbortController();
+ // A failed Handback must leave the current control viewer intact.
+ desktopRequest?.abort(); desktopRequest = request;
+ if (!prewarm) { desktopChanging = true; controls(); }
+ let released = false, viewer;
+ const requestJSON = async (path, body) => {
+ const value = await client.json(path, { method: 'POST', body, signal: request.signal });
+ if (request.signal.aborted || generation !== epoch || session?.id !== selectedSession) throw new DOMException('Aborted', 'AbortError');
+ return value;
+ };
+ if (!prewarm) text('desktop-status', handback ? '正在交还控制并切回观看…' : mode === 'control' ? '正在打开接管窗口…' : '正在切换到观看…');
+ try {
+ if (handback) {
+ await requestJSON(`${sessionPath(selectedSession)}/handback`, {});
+ released = true;
+ }
+ disposeCurrentViewer();
+ const value = await requestJSON(`${sessionPath(selectedSession)}/desktop`, {
+ mode, replace_own_control: !prewarm, ...(prewarm ? { existing_only: true } : {}),
+ });
+ const iframe = document.createElement('iframe');
+ iframe.title = mode === 'control' ? '机器人桌面 · 接管' : '机器人桌面 · 观看';
+ iframe.referrerPolicy = 'no-referrer'; iframe.src = desktopURL(value.url, location.origin);
+ iframe.hidden = prewarm;
+ viewer = { iframe, sessionId: selectedSession, epoch: generation, mode, prewarm, connected: false, firstFrame: false, expiresAt: 0 };
+ currentViewer = viewer;
+ await new Promise((resolve, reject) => {
+ const finish = error => {
+ clearTimeout(timer); iframe.removeEventListener('load', loaded);
+ viewer.cancelLoad = undefined;
+ if (error) reject(error); else resolve();
+ };
+ const loaded = () => {
+ try {
+ const body = iframe.contentDocument?.body?.textContent ?? '';
+ if (body.trim().startsWith('{') && JSON.parse(body).error) throw new Error('桌面授权已变化,请重新点击接管;其他使用者持有控制时需要先交还。');
+ finish();
+ } catch (error) { finish(error); }
+ };
+ const timer = setTimeout(() => finish(new Error('桌面查看器加载超时,请重新点击观看或接管。')), 30000);
+ viewer.cancelLoad = () => finish(new DOMException('Aborted', 'AbortError'));
+ iframe.addEventListener('load', loaded);
+ $('desktop').replaceChildren(iframe);
+ });
+ if (!prewarm && generation === epoch && currentViewer === viewer) {
+ text('desktop-status', viewer.firstFrame ? (mode === 'view' ? '只读画面已就绪。' : '接管画面已就绪。') : handback ? '控制已交还,当前为只读观看;画面连接由查看器显示。' : `${mode === 'control' ? '已签发接管授权' : '已签发观看授权'};画面连接由查看器显示。${value.pauses_agent === false ? '接管不会暂停机器人。' : '机器人暂停状态未确认。'}`);
+ if (handback) status('控制已交还,已切回观看。');
+ }
} catch (error) {
+ if (viewer && currentViewer === viewer) disposeCurrentViewer();
+ if (prewarm) return;
if (generation !== epoch || error.name === 'AbortError') throw error;
- const message = released ? '控制已交还,但观看重连失败。请点击“观看”重试。' + error.message : error.message;
+ const detail = error.code === 'desktop_busy' ? '桌面已有控制窗口,或控制权刚刚发生变化。同一密钥可重新点击接管;其他使用者需要先交还控制。' : error.message;
+ const message = released ? '控制已交还,但观看重连失败。请点击“观看”重试。' + detail : detail;
text('desktop-status', message); throw new Error(message);
+ } finally {
+ if (desktopRequest === request) desktopRequest = undefined;
+ if (!prewarm && generation === epoch) { desktopChanging = false; controls(); }
}
- finally { if (generation === epoch) { desktopChanging = false; controls(); } }
}
bind('view', () => desktop('view')); bind('control', () => desktop('control'));
bind('applications-refresh', async () => {
@@ -430,7 +608,7 @@ for (const [id, mode] of [['application-view', 'view'], ['application-control',
popup.opener = null;
desktopChanging = true; controls();
void run(id, async () => {
- try { const value = await post(`${sessionPath(session.id)}/desktop`, { mode, ...(mode === 'control' ? { replace_own_control: true } : {}), target: { type: 'application', application_id: selected } }); popup.location.href = desktopURL(value.url, location.origin); if (mode === 'control') $('desktop').replaceChildren(); status(mode === 'control' ? '应用接管窗口已打开,已替换当前密钥的旧控制窗口。' : '应用观看窗口已打开。'); }
+ try { const value = await post(`${sessionPath(session.id)}/desktop`, { mode, ...(mode === 'control' ? { replace_own_control: true } : {}), target: { type: 'application', application_id: selected } }); popup.location.href = desktopURL(value.url, location.origin); if (mode === 'control') disposeDesktop(); status(mode === 'control' ? '应用接管窗口已打开,已替换当前密钥的旧控制窗口。' : '应用观看窗口已打开。'); }
catch (error) { popup.close(); throw error; }
finally { desktopChanging = false; controls(); }
});
diff --git a/tools/node-agent-api/web/client.js b/tools/node-agent-api/web/client.js
index f85e375..9eb85df 100644
--- a/tools/node-agent-api/web/client.js
+++ b/tools/node-agent-api/web/client.js
@@ -1,5 +1,48 @@
export const sessionPath = id => `/v1/agents/sessions/${encodeURIComponent(id)}`;
+export function createPaintQueue(paint, {
+ requestFrame = requestAnimationFrame, cancelFrame = cancelAnimationFrame,
+ setTimer = setTimeout, clearTimer = clearTimeout, isHidden = () => document.hidden,
+} = {}) {
+ let pending;
+ const reset = () => {
+ if (!pending) return;
+ cancelFrame(pending.frame); clearTimer(pending.timer); pending = undefined;
+ };
+ const flush = () => { reset(); paint(); };
+ return {
+ schedule() {
+ if (pending) return;
+ const ticket = {}; pending = ticket;
+ const run = () => { if (pending === ticket) flush(); };
+ ticket.frame = requestFrame(run);
+ // rAF can stop in background tabs. State ingestion never waits for paint.
+ ticket.timer = setTimer(() => { if (isHidden()) run(); }, 100);
+ },
+ flush, reset,
+ };
+}
+
+export function createRefresh(load) {
+ let active;
+ const refresh = () => {
+ if (active) { active.dirty = true; return active.promise; }
+ const flight = { dirty: false }; active = flight;
+ flight.promise = (async () => {
+ try {
+ do {
+ flight.dirty = false;
+ try { await load(() => active === flight && !flight.dirty); }
+ catch (error) { if (active === flight && !flight.dirty) throw error; }
+ } while (active === flight && flight.dirty);
+ } finally { if (active === flight) active = undefined; }
+ })();
+ return flight.promise;
+ };
+ refresh.reset = () => { active = undefined; };
+ return refresh;
+}
+
export function capabilities(value = {}) {
return {
send: value.writes_enabled === true,
diff --git a/tools/node-agent-api/web/viewer-frame.js b/tools/node-agent-api/web/viewer-frame.js
new file mode 100644
index 0000000..94cdedb
--- /dev/null
+++ b/tools/node-agent-api/web/viewer-frame.js
@@ -0,0 +1,32 @@
+export function observeFirstFrame(rfb, notify, { requestFrame = requestAnimationFrame, cancelFrame = cancelAnimationFrame } = {}) {
+ const handleRect = rfb._handleRect, framebufferUpdate = rfb._framebufferUpdate;
+ let rectangles = 0, complete = false, disposed = false, frame;
+ function rect(...args) {
+ const encoding = this._FBU.encoding;
+ const result = handleRect.apply(this, args);
+ if (result && encoding >= 0 && this._FBU.width > 0 && this._FBU.height > 0) rectangles++;
+ return result;
+ }
+ function update(...args) {
+ const result = framebufferUpdate.apply(this, args);
+ if (result && rectangles > 0 && !complete && !disposed) {
+ complete = true;
+ const show = () => {
+ if (disposed) return;
+ if (rfb._display.pending()) { frame = requestFrame(show); return; }
+ notify({ width: rfb._fb_width, height: rfb._fb_height });
+ };
+ frame = requestFrame(show);
+ }
+ if (result) rectangles = 0;
+ return result;
+ }
+ rfb._handleRect = rect;
+ rfb._framebufferUpdate = update;
+ return () => {
+ disposed = true;
+ if (frame !== undefined) cancelFrame(frame);
+ if (rfb._handleRect === rect) rfb._handleRect = handleRect;
+ if (rfb._framebufferUpdate === update) rfb._framebufferUpdate = framebufferUpdate;
+ };
+}
diff --git a/tools/node-agent-api/web/viewer.js b/tools/node-agent-api/web/viewer.js
new file mode 100644
index 0000000..518c4a5
--- /dev/null
+++ b/tools/node-agent-api/web/viewer.js
@@ -0,0 +1,41 @@
+import RFB from '@novnc/core/rfb.js';
+import { observeFirstFrame } from './viewer-frame.js';
+
+const configuration = JSON.parse(document.getElementById('node-viewer-config').textContent);
+const target = document.getElementById('screen');
+const status = document.getElementById('connection-status');
+const url = new URL(configuration.socketPath, location.origin);
+url.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
+const report = (type, extra = {}) => parent.postMessage({ source: 'node-agent-viewer', type, mode: configuration.mode, expiresAt: configuration.expiresAt, ...extra }, location.origin);
+let disposed = false;
+report('ready');
+const rfb = new RFB(target, url.href);
+rfb.viewOnly = configuration.mode !== 'control';
+rfb.scaleViewport = true;
+rfb.resizeSession = false;
+const stopFrame = observeFirstFrame(rfb, dimensions => {
+ if (disposed) return;
+ status.hidden = true;
+ report('first-frame', dimensions);
+});
+rfb.addEventListener('connect', () => { if (!disposed) { status.textContent = '正在接收桌面画面…'; report('connected'); } });
+rfb.addEventListener('disconnect', () => {
+ stopFrame();
+ if (disposed) return;
+ status.hidden = false; status.textContent = '桌面连接已断开,请重新打开。';
+ report('disconnected');
+});
+rfb.addEventListener('securityfailure', () => {
+ status.hidden = false; status.textContent = '桌面连接验证失败,请重新打开。';
+});
+function dispose() {
+ if (disposed) return;
+ disposed = true;
+ stopFrame();
+ rfb.disconnect();
+ navigator.sendBeacon(new URL('close', location.href).href, '');
+}
+addEventListener('message', event => {
+ if (event.origin === location.origin && event.source === parent && event.data?.source === 'node-agent-parent' && event.data.type === 'dispose') dispose();
+});
+addEventListener('pagehide', dispose, { once: true });