Dinoer

Technical choices

Seven decisions that shape Dinoer, each with the reason behind it and the price it costs. Including the one a real leak forced, the same day it was found.

Every choice below costs something. Listing the benefit without the price would make this page an advertisement rather than a document you can evaluate.

Text over pixels, with nothing kept in reserve

Diwall, the tool Dinoer forked from, treats vision as a fallback for elements with no semantic representation. Dinoer does not keep that fallback: the entire perception layer — screenshots, Set-of-Mark, the vision model — was removed in a from-scratch reconstruction, not made optional. There is no flag that brings it back.

The price. A clickable canvas or an image with no accessible label is outside what Dinoer can act on, full stop, where the tool it forked from could still fall back to a vision model. That trade was made on purpose: the tasks Dinoer targets — reading and synthesising public text — never needed it, and carrying the capability anyway would have carried its cost (a local model to run, a remote one to be careful with) for a class of pages this project does not target.

Escalate only when a page actually needs it

campagne.py reads most pages with requests + BeautifulSoup — no browser, no JavaScript, a fraction of the cost of a full Playwright session. A page only escalates to the retained browser core (rpa.py/shot.py) when the light tier marks it insuffisante_legere — a JS-rendered shell with nothing readable in the raw HTML.

The price. The escalation trigger currently keys off “empty text,” not “text present but unusable.” A real campaign found two pages — a GitHub profile, a YouTube channel — that returned text and so never escalated, and that text was cookie-banner noise with nothing else in it. Stated as a real, open architecture gap, not a hidden one. The full case →

One process per call, not a server

Dinoer is a command, not a service. shot.py and rpa.py start a Playwright process, do what the scenario says, answer once and exit: no script of the product listens on a port between two calls. Each call writes one entry to the operations journal, success or failure, and each call meets the guide lock — with neither a valid --guide-version token nor a local marker, it is refused.

campagne.py has another shape: one process that runs a whole manifest, calling its light fetch in-process and rpa.py as a subprocess for the pages that need a browser, then ends. What it keeps from one step to the next is files: the corpus, the operations journal and the search cache.

The price. Live page state does not survive between two calls. Cookies and local storage can be saved and resumed (--sauver-session, --reprendre-session); an open modal, a half-filled form or a scroll position cannot. A sequence that depends on such state has to be one scenario, in one process — the recipe on lost sessions shows what it looks like when it is not.

What this choice does not change. The transport does not decide where a secret is resolved: it is inside the Playwright process, whatever launched it. A protocol decides nothing; the agent that drives the tool does. And a hosted model that reads an output receives it, whatever carried it there.

Local collection, a delegated synthesis you must choose carefully

Collection stays on your machine: SearXNG (local or remote, your choice), requests, and the retained browser core all run where Dinoer runs. Report synthesis does not — it is delegated to OpenCode or a local Ollama model, and OpenCode’s free-tier models are the default reasoning backend. Collected page text can transit to whichever back-end you configure.

The price. This is not a local-only guarantee for the whole pipeline, and this page will not pretend it is. If a source is sensitive, review lib/modeles.py and choose the back-end deliberately before pointing Dinoer at it — the same discipline opencode.jsonc enforces for tool access applies here to the destination of the text itself.

The corpus is what the model is allowed to leave — and it left anyway, once

opencode.jsonc, project-local, denies the delegated model its own websearch/webfetch tools — and since 1.0.1 the same denial travels in OPENCODE_CONFIG_CONTENT on every opencode run Dinoer starts, whatever the directory. It exists because, on 14 August 2026, a real campaign caught the model doing exactly what it denies now: twelve live web searches, sourcing content absent from the collected corpus, invisible in the final answer and found only by capturing the full event stream rather than trusting the returned text.

The price, stated rather than hidden. bash stays allowed — some scenarios genuinely need it, to curl a notification — and a model denied websearch has, in a real verification run, reached the live web through bash curl instead. Not a sealed guarantee. A reduced surface, and the honest way to describe it to an operator is exactly that phrase, not “contained.”

Signals, never verdicts

Dinoer reports what it noticed and lets you decide. etat.pret_a_agir: false means a friction was perceived — a WAF signature, a session drift — not a refusal to work. WAF detection is keyword-based and can be wrong; it is exposed as a count, never as an exception that halts a run.

The price. You have to read the answer. A caller that looks only at succes gets no benefit from any of these signals.

The lock that makes your first run fail

Dinoer refuses to execute until whoever drives it has read the guide and can quote its version number.

Dinoer is not in any model’s training data, and a model that has not read the guide improvises. That is not hypothetical — it caused a documented security violation in this project’s predecessor, and this lock is the answer. What improvising looks like, and the two rules that prevent it →

The price. Your first call fails, deliberately, with {"succes": false, "erreur": "guide_non_lu"}. Every documented command carries --guide-version. It is friction, it is on purpose.

And it is never translated. The guide exists in English only. A translated guide can desynchronise silently — its version number resynchronised mechanically while its content still describes the previous version — and an agent then passes the lock having read obsolete instructions. A model reads English natively and English costs fewer tokens: the benefit of translating it is nil, the risk is real.

In short

  • No vision fallback anywhere, on purpose — not a smaller version of Diwall’s, an absent one.
  • Escalate to a browser only when the light tier cannot read a page.
  • One process per call, not a server — so a sequence that needs live page state is one scenario.
  • Collection is local; synthesis is delegated — review the back-end before a sensitive source.
  • The corpus boundary is enforced, not merely assumed — and its real gap is published, not smoothed over.
  • Signals rather than verdicts — you decide, so you must read.
  • The first run fails on purpose, and the guide that unlocks it is never translated.