// The hands for when a page will not simply be read. // // Live, 2026-09-11: a Workshop build spent 122 steps on a Reddit listing, got nothing, and reasoned // its own way to "the read tool truncates, so I need to run a script" and "I should fetch the JSON // feed directly" — then announced "I see http-request is available". It was not. It spent the rest of // its budget on a tool that never existed. The gap was not the site's: a browser that cannot read // structured data, press a key, wait for a lazy list, pick a dropdown value or follow a new tab meets // a wall on every second site. These tests pin the six hands that closed it, and — the part that // matters most — that a script may READ the page and never ACT on it, so the owner's approval gate // can never be walked around by a snippet. import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import agent from '../src/agent.js'; import roles from '../src/roles.js'; const agentSrc = readFileSync(fileURLToPath(new URL('../src/agent.js', import.meta.url)), 'utf8'); const serverSrc = readFileSync(fileURLToPath(new URL('../src/server.js', import.meta.url)), 'utf8'); const names = () => agent.TOOLS.map((t) => t.function && t.function.name); const NEW = ['run_script', 'fetch_data', 'press_key', 'wait_for', 'choose_option', 'tabs', 'switch_tab']; describe('the browser has the hands a person at a laptop has', () => { it('offers all six, each with a described shape', () => { for (const n of NEW) { const t = agent.TOOLS.find((x) => x.function && x.function.name === n); expect(t, n + ' is in the palette').toBeTruthy(); expect(String(t.function.description).length).toBeGreaterThan(40); expect(t.function.parameters.type).toBe('object'); } // and every one of them is dispatched, not merely advertised for (const n of NEW) expect(agentSrc, n + ' has a handler').toMatch(new RegExp(`case '${n}'`)); }); it('gives them to EVERY role — including one with its own tool list', () => { // A role naming its own tools would not see a newly added one, and the symptom (an agent that // mysteriously will not use a tool) is miserable to diagnose. So they sit with look() and read(). for (const role of ['general', 'research.reddit', 'qa.web']) { const got = roles.toolsFor(role, agent.TOOLS).map((t) => t.function.name); for (const n of ['run_script', 'fetch_data', 'press_key', 'wait_for']) { expect(got, `${role} can reach ${n}`).toContain(n); } } }); it('publishes the palette, so nobody composing a role has to guess a tool name', () => { expect(serverSrc).toMatch(/app\.get\('\/v1\/agent\/tools'/); expect(serverSrc).toMatch(/takes: Object\.keys/); }); it('gives data a bigger budget than prose, and says so when it still does not fit', () => { expect(agentSrc).toMatch(/const DATA_CAP = 24000;/); expect(agentSrc).toMatch(/observeData/); expect(agentSrc).toMatch(/more characters — return less/); // the prose observation is untouched /* The prose cap is still 6000. observe() also writes the page into the journal now (see remember() beside it), so the exact one-liner is no longer the thing to pin - the cap is. */ expect(agentSrc).toMatch(/const observe = \(text\) => \{ messages\.push\(\{ role: 'tool', content: String\(text\)\.slice\(0, 6000\) \}\);/); expect(agentSrc).toMatch(/const CONTENT_CAP = 6000;/); }); }); describe('a script may READ the page; it may never ACT on it', () => { const refused = (src) => agent.scriptRefusal(src); it('allows the reading a scout actually needs', () => { for (const ok of [ "return [...document.querySelectorAll('article')].map(a => ({ title: a.querySelector('h3')?.innerText, url: a.querySelector('a')?.href }))", 'return JSON.parse(document.querySelector("#data").textContent).items.length', 'document.title', "return [...document.querySelectorAll('tr')].map(r => [...r.cells].map(c => c.innerText))", 'return window.__NEXT_DATA__ && Object.keys(window.__NEXT_DATA__)', ]) expect(refused(ok), ok.slice(0, 50)).toBeNull(); }); it('refuses every way a snippet could act instead of read — and names the tool that does it properly', () => { const cases = [ ['return fetch("/api/x").then(r => r.json())', /fetch_data/], ['const x = new XMLHttpRequest(); return x', /fetch_data/], ['navigator.sendBeacon("/x")', /fetch_data/], ['return await import("/evil.js")', /fetch_data/], ['document.querySelector("button").click()', /owner's approval gate/], ['document.forms[0].submit()', /owner's approval gate/], ['el.dispatchEvent(new MouseEvent("click"))', /click, type or act/], ['return document.cookie', /session token must never leave/], ['localStorage.setItem("a", 1)', /may not write storage/], ['window.open("https://x.com")', /may not navigate/], ['location.href = "https://x.com"', /may not navigate/], ['location.assign("https://x.com")', /may not navigate/], ['document.body.innerHTML = "