Eduardo Silveira

An Agent You Cannot Watch

Gaithano Speak has a third mode besides dictation and answering questions. The model gets tools and drives the machine: screenshot, decide, maybe confirm, act, screenshot again. It clicks things.

Everything I learned building it is about rails rather than capability. The capability was never the hard part.

The risk policy is pure, and that is the point

Classification of an action as safe, needing confirmation, or blocked lives in a pure, unit-tested function in the core package. The executor contains no approval logic at all. By the time it is called, the decision has already been made somewhere it can be tested.

The temptation, every single time, is a "just this once" path that skips classification for something obviously fine. Don't. The value of the boundary is that it is total. A policy with one bypass is a policy you now have to reason about at every call site.

The same reasoning put blocked above auto-confirm mode in the check order. Turning on "don't ask me" can never disarm the password-field rail. There is a test named for exactly that, and it is not incidental.

The bug that green tests missed

Risky key chords are matched against a set. I normalised the incoming chord by sorting the keys, then compared it against the raw set.

cmd+shift+delete sorts to cmd+delete+shift. It did not match. It was classified safe.

Every test passed, because the tests used chords that happened to already be in sorted order. Both sides of a comparison have to be normalised, and "both sides" is the kind of thing you write in a comment afterwards because the code reads fine without it.

Coordinates lie

The model sees a downscaled JPEG, so its coordinates are in image space. The mouse lives in screen space, and macOS has two different screen spaces depending on which API you ask.

Use display bounds with a top-left origin, the same space synthetic events use, not the screen frame, which flips Y on non-primary displays. Mix them and the agent clicks somewhere plausible but wrong, which looks exactly like a model that is bad at finding buttons. I spent real time blaming the model.

The screen is attacker-controlled

This is the part I would underline. An agent that can see the screen can see every window on it, and every one of those is text somebody else wrote. A web page, a chat message, a README open in another editor. All of it reaches the model, and all of it can be phrased as an instruction.

So the trust boundary is a separate constant in the prompt builder, asserted verbatim in a test, specifically so that rewording the task description can never quietly drop it. That is a floor rather than a guarantee. The actual rail is the confirmation policy: the model is assumed to be persuadable, and the things it can do without asking are chosen on that assumption.

Taking the panel down ends the run

Small rule, disproportionate importance. The step list is the only place a person can see what the agent is doing, so closing it stops the agent. A run that outlives its own visibility is precisely the failure mode all the rest of this is guarding against.