Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions articles/extracting-a-category-to-csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,6 @@ The screenshots above are the browser's own captures from the run, returned
by `browser_take_screenshot` at steps 4 and 5. The session ran headless, as
MCP sessions do; the browser's screenshots are the visual record.

## Recorded against

| Piece | Version |
|---|---|
| invisible-playwright-mcp | 0.3.0 (the locally installed server that drove this run; releases have moved well past it, so your tool names may differ) |
| invisible_playwright | 0.8.3 |
| invisible_core | 26.17.0 |
| Engine | firefox-26 |

Run date: 2026-09-03. The tool names and response shapes above are this
version's; if yours differ, check your versions before assuming the page
changed.

## Reproducing it

Attach the browser to your assistant (from the
Expand Down
12 changes: 0 additions & 12 deletions articles/web-research-audited/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,6 @@ rendered as images are where reading starts to drift. The honest conclusions:
bigger one** - and a mismatched audit is how you find out for the cost of a
sandbox run instead of a wrong decision.

## Recorded against

| Piece | Version |
|---|---|
| invisible-playwright-mcp | 0.3.0 (the locally installed server that drove this run; releases have moved well past it, so your tool names may differ) |
| invisible_playwright | 0.8.3 |
| invisible_core | 26.17.0 |
| Engine | firefox-26 |

Run date: 2026-09-04. Both runs, agent and audit, on the same date against
the same pages.

## Reproducing it

Attach the browser to your assistant
Expand Down
4 changes: 1 addition & 3 deletions docs/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ a Firefox patched at the C++ source. The server ships inside the `aihawk`
package and is what `aihawk` runs with no subcommand: `uvx aihawk` is what a
client registers, `python -m aihawk` is what the interface spawns. Every tool
below is a thin wrapper over the engine, and the interface (`aihawk ui`) is a
client of it like any other. The PyPI name `invisible-playwright-mcp` lives on
as a shim over `aihawk`, so a client that registered
`uvx invisible-playwright-mcp` before 0.11.0 keeps working unchanged.
client of it like any other.

**How to install this, and the two ways to use it, are in
[AIHawk's README](https://github.com/feder-cr/AIHawk#readme).** This page keeps
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "aihawk"
version = "0.11.0"
version = "0.11.1"
description = "Drive a stealth browser with an LLM from one command"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
23 changes: 11 additions & 12 deletions src/aihawk/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
ONE loop. There were briefly two, which is how a README sentence saying "same
machinery" becomes false without anybody editing it: the second copy gets a fix,
the first does not, and the two answers diverge for a task that looks identical
from outside. They were merged, and later the second CALLER went too - the
`aihawk do` subcommand, removed on 2026-09-03 - so this loop now has exactly one
consumer in the product and one in the tests.
from outside. They were merged, so this loop now has exactly one consumer in
the product (the interface, via `brain.py`) and one in the tests (`run_task`).

The narration is a parameter rather than a mode. `do` passes a sink that drops
everything, `ui` passes the thing that pushes events to the page, and neither
appears in here. A loop that knows whether it is being watched is a loop with two
The narration is a parameter rather than a mode. The interface passes the
callback that pushes events to the page; `run_task` passes nothing and gets
silence. A loop that knows whether it is being watched is a loop with two
behaviours to test.
"""
from __future__ import annotations
Expand All @@ -31,7 +30,8 @@


async def _silent(_kind: str, _text: str) -> None:
"""The default narrator: says nothing, so `do` prints only its answer."""
"""The default narrator: says nothing, which is what `run_task` wants -
an answer, not a transcript."""


def mcp_tools_to_openai(tools) -> List[dict]:
Expand Down Expand Up @@ -165,11 +165,10 @@ async def run_task(mcp, task: str, *, client, model: str, max_turns: int = 25,
max_tokens: int = Conversation.MAX_TOKENS) -> str:
"""One instruction, one answer, no narration.

Four lines over `Conversation`, and no longer called by the product: the
`aihawk do` subcommand it was written for was removed on 2026-09-03. It is
kept because the suite drives the loop through it - about twenty-five tests
in test_agent_loop.py - and rewriting all of them onto `Conversation`
directly would move a lot of code to delete four lines.
Four lines over `Conversation`, not called by the product - kept because
the suite drives the loop through it, about twenty-five tests in
test_agent_loop.py, and rewriting all of them onto `Conversation` directly
would move a lot of code to delete four lines.
"""
tools = (await mcp.list_tools()).tools
convo = Conversation(client, model, max_turns=max_turns, max_tokens=max_tokens)
Expand Down
4 changes: 1 addition & 3 deletions src/aihawk/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

The connection outlives any single instruction, because the browser has to still
be there when the next line is typed and the live pane has to keep watching it
in between. There was a second, shorter-lived form of this - `runner.drive`,
one task then close, behind an `aihawk do` subcommand - and both were removed on
2026-09-03: one way in is the whole point of the page this serves.
in between. One way in is the whole point of the page this serves.

WHY THIS IS A CLIENT AND NOT AN IMPORT. The same shell used to run inside the MCP
server process and reach the browser through `registry`, which is a Python object
Expand Down
3 changes: 1 addition & 2 deletions src/aihawk/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
Spawning is `Link`'s. This module used to build its own StdioServerParameters as
well, which meant two places knew the command, the arguments and the environment
of the child - and a change to how the server is launched had to be made twice
or be wrong once. It also held `drive`, the one-task-then-close half of an
`aihawk do` subcommand, removed on 2026-09-03 along with it.
or be wrong once.
"""
from __future__ import annotations

Expand Down
2 changes: 1 addition & 1 deletion tests/test_agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
The scripted client mirrors the surface `llm.make_client` returns, which is
`openai.OpenAI`: the loop only ever touches `client.chat.completions.create`.

The scripted MCP session mirrors `mcp.ClientSession` as `runner.drive` uses
The scripted MCP session mirrors `mcp.ClientSession` as `Link` uses
it: `await mcp.list_tools()` returning a ListToolsResult, and
`await mcp.call_tool(name, args)` returning a CallToolResult.

Expand Down
16 changes: 0 additions & 16 deletions tests/test_cli_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,6 @@
CANARY on purpose so any echo of it, whole or truncated after the prefix, is
recognisable; test_the_key_detector_is_not_vacuous proves the detector can
actually see a leak instead of always printing PASS.

⛔ THIS FILE USED TO DRIVE `aihawk do`, WHICH NO LONGER EXISTS. The subcommand
was removed on 2026-09-03: one way in, and it is `ui`. Most of what was here
tested the option surface and the key handling, and both are still real, so
those tests moved onto `ui` rather than being deleted with the command. Four
guarantees genuinely died with it and are recorded here so nobody hunts for
them later:

* "no key anywhere exits 1 and names both ways to supply one" - dead with
`do`, reborn with 0.4.0: the literal-command placeholder was removed, so
`ui` now refuses without a key too, and the refusal must also point at
the invisible_playwright library as the model-free way to drive the
engine (tested below);
* "the TASK argument is required", "the task reaches drive verbatim" and "the
result is printed verbatim with one trailing newline" - `ui` takes no task
and prints no result.
"""
from __future__ import annotations

Expand Down
5 changes: 0 additions & 5 deletions tests/test_key_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,6 @@ async def test_the_link_hands_the_child_the_scrubbed_environment(monkeypatch):
of which leave every child_env test above green while the key ships to the
child (the first) or every STEALTHFOX_* option silently stops working (the
second).

⛔ It used to run through `runner.drive`, behind the `aihawk do` subcommand
removed on 2026-09-03. `drive` was only ever a Link opened and closed around
one task, so the guarantee never belonged to it: it belongs to `Link`, which
is what the interface uses, and that is where it is asserted now.
"""
monkeypatch.setenv(KEY_NAME, KEY)
captured = {}
Expand Down
12 changes: 4 additions & 8 deletions tests/test_real_cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
"""A real browser, a real model, and one factual question about a page.

⛔ THIS USED TO INVOKE `aihawk do`, removed on 2026-09-03. What it was actually
proving was never the subcommand: it was that the loop, given a real browser and
a real model, reads a page and answers what is on it. That is the whole product
in one assertion, and it survives the command that used to carry it.

So it now opens the same `Link` the interface opens and runs the same loop the
interface runs. What is lost with `do` is the CLI wiring around it, and that is
covered without a browser or a key in test_cli_surface.py.
The loop, given a real browser and a real model, reads a page and answers
what is on it - the whole product in one assertion. It opens the same `Link`
the interface opens and runs the same loop the interface runs; the CLI wiring
around that is covered without a browser or a key in test_cli_surface.py.

Skipped unless both a real binary and a real key are present, because it spends
money and launches Firefox.
Expand Down
10 changes: 5 additions & 5 deletions tests/test_ui_drive.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""UI-level drive: a real local page, a real MCP server, a real browser.

Every test here serves its own page over http from 127.0.0.1, spawns
`aihawk.mcp` exactly the way `aihawk.runner.drive` spawns it
(same `child_env`, same `StdioServerParameters`), and then checks what
happened INSIDE the page rather than what the tool said about itself. A tool
Every test here serves its own page over http from 127.0.0.1, spawns the
server the same way `Link` does (`python -m aihawk`, same `child_env`, same
`StdioServerParameters`), and then checks what happened INSIDE the page
rather than what the tool said about itself. A tool
that answers "clicked #go" while nothing moved is the failure this file exists
to catch, so the tool's own success string is never the assertion.

Expand All @@ -14,7 +14,7 @@

RUN THEM WITH (they are deselected by default, see `addopts` in pyproject):

C:/tmp/venv_aihawk/Scripts/python -m pytest -m ui -q C:/src/firefox-stealth/release/aihawk/pkg-cli/tests/test_ui_drive.py
C:/tmp/venv_aihawk/Scripts/python -m pytest -m ui -q C:/src/firefox-stealth/release/aihawk/tests/test_ui_drive.py

Serially, and on a machine with no other browser bench running: they launch ONE
browser for the whole module and reuse it, which is also why each test starts
Expand Down
Loading