Skip to content

browser_read bypasses the Google sign-in gate; flight-table shortcut answers unrelated questions; no LICENSE #1

Description

@MrJev

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions