Hi — reviewing jot for a write-up on mrjev.com, at 55c3082. I ran the workspace in node:22 and then offline against a local stand-in, so no real Jev calls.
Before the findings: the browser safety work here is better than most production code I read. snapshot.ts opening with /** Static page-owned code; model output never becomes a selector or script. */ and then actually holding to it — opaque integer ids in a WeakMap, an in-page guard fingerprint re-verified before every interaction, elementFromPoint occlusion tests — is the right design. browser_fill restricting typed text to browserTextCandidates(user.content) so the model can only choose among things the user actually said is a genuinely clever constraint. navigationURL() re-validating scheme and rejecting embedded credentials on every act, openHome and manual-login path is the kind of thing people skip. And password/file/hidden exclusion is done at three independent layers, including on the path that names a target directly.
That last point is why the first finding below reads as an oversight rather than a design position.
1. browser_read bypasses the Google sign-in gate that every other browser tool honours.
The gate lives in the shared result builder, server/browser-tools.ts:8:
function result(snapshot:BrowserSnapshot,allowSignIn=false):ToolResult{
if(!allowSignIn&&/accounts\.google\.com/i.test(snapshot.url))return {status:'error',reason:'needs_input',error:'Google sign-in is open.',…};
observe, navigate, search, click, fill, select, open, scroll and wait all return result(...). browser_read calls it only on the interruption branch (:107):
const snapshot=await browser.observe(signal);if(snapshot.interruption)return result(snapshot);
…
return {status:'ok',text:text||'No matching page content.',data:{source:'browser',url:snapshot.url,field:args.field}};
interruption requires a visible captcha/challenge iframe and matching blocking text (snapshot.ts:30-31). A Google sign-in page has neither, so the URL check is never reached. Feeding one snapshot of accounts.google.com to both tools:
--- browser_observe ---
{"status":"error","reason":"needs_input","error":"Google sign-in is open.", …}
--- browser_read field=text, SAME page ---
{"status":"ok","text":"Sign in\nUse your Google Account\nEmail or phone\nvictim@example.com\nForgot email?\n…"}
--- browser_read field=url, SAME page ---
{"status":"ok","text":"https://accounts.google.com/v3/signin/identifier?continue=https://mail.google.com/"}
Two amplifiers:
browser_read is in the replyable set (server/jev-agent.ts:8), so that text can be returned to the user unchanged through respond_N.
- It is the latest tool result, and
jev-agent.ts:26 exempts the latest from clipping — const clipped=m===latestTool||!text||text.length<=1200?text:… — so it goes upstream in full rather than at 1200 characters.
The browser runs on a shared persistent profile (server/browser.ts:5), so a logged-in account chooser is a realistic state. server/browser-tools.test.ts:66 covers this gate and passes, because it exercises observe rather than read.
Routing read through result() — or hoisting the URL check into browser.observe() — would close it in one line.
Separately: allowSignIn has no caller that ever passes true, so it's currently a dead parameter. When a user does ask to sign in, wantsSignIn correctly unlocks the auth controls at :6,:60,:93, but landing on accounts.google.com still hard-errors. The feature looks half-built in both directions.
2. A hardcoded flight-table regex can answer a question nobody asked.
server/local-draft.ts:49:
const table=flightTable(messages.filter(m=>m.role==='tool').map(m=>m.result.text??'').join('\n'));
if(table){
const text=`Here are the matching flight options:\n\n${table}`;
yield {type:'text_delta',delta:text};
return {status:'ok',text,reason:'final',…};
}
The trigger is server/flight-table.ts:2 matching two or more rows in any tool text. The user's question is never read. Reproduced with a user asking about baggage policy:
local model fetch calls: 0
reason: final provider: evidence-table
answer: Here are the matching flight options:
| Depart | Arrive | Airline | Duration | Stops | Price |
| 7:40 AM | 8:35 AM | British Airways | 1 hr 55 min | Nonstop | $178 |
reason:'final' then terminates the loop (loop.ts:36). The output is confident, well formatted and unrelated to the question. Gating it on the question actually being about flights — or dropping the shortcut and letting the drafter see the table as evidence — would fix it.
3. npm test && npm run build fails at HEAD.
README.md:68 gives that exact command. On a clean install:
# tests 103 / # pass 88 / # fail 1 / # skipped 13 EXIT=1
not ok 96 - draft_message delegates only its text generation while Jev still selects actions
npm run typecheck -> loop.ts(20,24) TS2322; browser-tools.ts(97,14) TS7006
npm run build -> exit 2
The failing test traces to server/jev-agent.ts:54-55:
if(action==='draft_message'&&!long&&!browsed)action='reply';
where long comes from planned.answers.needs_long_draft?.type==='noul'&&…, so a missing or malformed answer defaults to false and overturns an explicit draft_message decision. git log -L54,55:server/jev-agent.ts shows the !browsed clause and the now-failing test landed in the same commit (d3d2742). In production a missing noul throws at typesafe.ts:22, so the silent default only surfaces with an injected evaluator — but the override itself is live on every short non-browsing turn.
A CI job running the README's own command would have caught this and the two type errors.
4. There's no licence.
No LICENSE file, and no license field in any of the five package.json files. Strictly, nobody can reuse any of this. Worth contrasting with packages/jev-core/src/data/conversation-words.LICENSE.md, where you documented the source commit SHA, a snapshot hash, itemised modifications and a scope note — that's better licence hygiene than most projects manage, which makes the repo's own absence stand out.
Smaller notes:
server/index.ts:13,23 treats any single-token .env as the API key and sends it as a bearer token, whatever it actually is. And resolve(".env") is CWD-relative.
- Three env-provided endpoints have no validation:
JOT_DRAFT_URL (index.ts:24, no loopback check — point it at a remote OpenAI-compatible endpoint and the whole conversation goes there), JOT_BROWSER_CDP, and JOT_BROWSER_VNC (also absent from .env.example, and opened with window.open). PORT=abc yields http://localhost:NaN.
- The README never mentions the browser: not that a real Chrome starts, not the persistent profile under
.cache/browser-profiles, not that google.com loads at startup before any user action, not that Google is the hardcoded search engine, and not that live page text and form field values go upstream. packages/browser is missing from the package list.
jev-agent.ts:24 strips data.elements from browser results but not data.fields, and fields carry each combobox/textbox value at .slice(0,500).
Happy to send a PR for the browser_read routing.
Hi — reviewing jot for a write-up on mrjev.com, at
55c3082. I ran the workspace innode:22and then offline against a local stand-in, so no real Jev calls.Before the findings: the browser safety work here is better than most production code I read.
snapshot.tsopening with/** Static page-owned code; model output never becomes a selector or script. */and then actually holding to it — opaque integer ids in aWeakMap, an in-pageguardfingerprint re-verified before every interaction,elementFromPointocclusion tests — is the right design.browser_fillrestricting typed text tobrowserTextCandidates(user.content)so the model can only choose among things the user actually said is a genuinely clever constraint.navigationURL()re-validating scheme and rejecting embedded credentials on every act, openHome and manual-login path is the kind of thing people skip. And password/file/hidden exclusion is done at three independent layers, including on the path that names a target directly.That last point is why the first finding below reads as an oversight rather than a design position.
1.
browser_readbypasses the Google sign-in gate that every other browser tool honours.The gate lives in the shared result builder,
server/browser-tools.ts:8:observe,navigate,search,click,fill,select,open,scrollandwaitall returnresult(...).browser_readcalls it only on the interruption branch (:107):interruptionrequires a visible captcha/challenge iframe and matching blocking text (snapshot.ts:30-31). A Google sign-in page has neither, so the URL check is never reached. Feeding one snapshot ofaccounts.google.comto both tools:Two amplifiers:
browser_readis in the replyable set (server/jev-agent.ts:8), so that text can be returned to the user unchanged throughrespond_N.jev-agent.ts:26exempts the latest from clipping —const clipped=m===latestTool||!text||text.length<=1200?text:…— so it goes upstream in full rather than at 1200 characters.The browser runs on a shared persistent profile (
server/browser.ts:5), so a logged-in account chooser is a realistic state.server/browser-tools.test.ts:66covers this gate and passes, because it exercisesobserverather thanread.Routing
readthroughresult()— or hoisting the URL check intobrowser.observe()— would close it in one line.Separately:
allowSignInhas no caller that ever passestrue, so it's currently a dead parameter. When a user does ask to sign in,wantsSignIncorrectly unlocks the auth controls at:6,:60,:93, but landing on accounts.google.com still hard-errors. The feature looks half-built in both directions.2. A hardcoded flight-table regex can answer a question nobody asked.
server/local-draft.ts:49:The trigger is
server/flight-table.ts:2matching two or more rows in any tool text. The user's question is never read. Reproduced with a user asking about baggage policy:reason:'final'then terminates the loop (loop.ts:36). The output is confident, well formatted and unrelated to the question. Gating it on the question actually being about flights — or dropping the shortcut and letting the drafter see the table as evidence — would fix it.3.
npm test && npm run buildfails at HEAD.README.md:68gives that exact command. On a clean install:The failing test traces to
server/jev-agent.ts:54-55:where
longcomes fromplanned.answers.needs_long_draft?.type==='noul'&&…, so a missing or malformed answer defaults to false and overturns an explicitdraft_messagedecision.git log -L54,55:server/jev-agent.tsshows the!browsedclause and the now-failing test landed in the same commit (d3d2742). In production a missing noul throws attypesafe.ts:22, so the silent default only surfaces with an injected evaluator — but the override itself is live on every short non-browsing turn.A CI job running the README's own command would have caught this and the two type errors.
4. There's no licence.
No LICENSE file, and no
licensefield in any of the fivepackage.jsonfiles. Strictly, nobody can reuse any of this. Worth contrasting withpackages/jev-core/src/data/conversation-words.LICENSE.md, where you documented the source commit SHA, a snapshot hash, itemised modifications and a scope note — that's better licence hygiene than most projects manage, which makes the repo's own absence stand out.Smaller notes:
server/index.ts:13,23treats any single-token.envas the API key and sends it as a bearer token, whatever it actually is. Andresolve(".env")is CWD-relative.JOT_DRAFT_URL(index.ts:24, no loopback check — point it at a remote OpenAI-compatible endpoint and the whole conversation goes there),JOT_BROWSER_CDP, andJOT_BROWSER_VNC(also absent from.env.example, and opened withwindow.open).PORT=abcyieldshttp://localhost:NaN..cache/browser-profiles, not thatgoogle.comloads at startup before any user action, not that Google is the hardcoded search engine, and not that live page text and form field values go upstream.packages/browseris missing from the package list.jev-agent.ts:24stripsdata.elementsfrom browser results but notdata.fields, and fields carry each combobox/textbox value at.slice(0,500).Happy to send a PR for the
browser_readrouting.