From 2080f71edbe99b4f1d9b1b5a6498306b2dc8d955 Mon Sep 17 00:00:00 2001 From: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:37:54 +0200 Subject: [PATCH 1/2] Remove the legacy notes about aihawk-do's removal and the dated version tables Historical asides that outlived their usefulness, at the owner's request: - The "Recorded against" version tables in both articles (invisible-playwright-mcp 0.3.0, engine firefox-26, dated 2026-09-03/04) - the "Reproducing it" section right below each one already gives a working command, and the table added a stale snapshot nobody needed to reproduce it. - Every code comment and test docstring explaining that `aihawk do` / `runner.drive` was removed on 2026-09-03 (agent.py, link.py, runner.py, test_key_isolation.py, test_real_cli.py, test_cli_surface.py). Where the surrounding paragraph carried a real architectural point (why run_task still exists, why the loop has one consumer, what test_real_cli.py actually proves), it is kept, rephrased without naming the removed command. - The PyPI-shim compatibility sentence in docs/mcp-server.md ("invisible-playwright-mcp lives on... before 0.11.0 keeps working unchanged") - the shim's own README, on the archived package, already documents this in full. Two more inaccuracies caught while touching this code, both stale references to the removed `drive`: test_ui_drive.py compared its spawn to `aihawk.runner.drive`, which does not exist, and named a dead `pkg-cli/tests/` path in its run-it-yourself line; test_agent_loop.py made the same comparison. Both now describe what actually spawns the server today. --- .../extracting-a-category-to-csv/README.md | 13 ----------- articles/web-research-audited/README.md | 12 ---------- docs/mcp-server.md | 4 +--- src/aihawk/agent.py | 23 +++++++++---------- src/aihawk/link.py | 4 +--- src/aihawk/runner.py | 3 +-- tests/test_agent_loop.py | 2 +- tests/test_cli_surface.py | 16 ------------- tests/test_key_isolation.py | 5 ---- tests/test_real_cli.py | 12 ++++------ tests/test_ui_drive.py | 10 ++++---- 11 files changed, 24 insertions(+), 80 deletions(-) diff --git a/articles/extracting-a-category-to-csv/README.md b/articles/extracting-a-category-to-csv/README.md index f0602d06e..51bb4a644 100644 --- a/articles/extracting-a-category-to-csv/README.md +++ b/articles/extracting-a-category-to-csv/README.md @@ -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 diff --git a/articles/web-research-audited/README.md b/articles/web-research-audited/README.md index 578132053..b10b34d8e 100644 --- a/articles/web-research-audited/README.md +++ b/articles/web-research-audited/README.md @@ -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 diff --git a/docs/mcp-server.md b/docs/mcp-server.md index 0e4ed635c..0c1cfa733 100644 --- a/docs/mcp-server.md +++ b/docs/mcp-server.md @@ -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 diff --git a/src/aihawk/agent.py b/src/aihawk/agent.py index 23a5358e8..0cbc9cb62 100644 --- a/src/aihawk/agent.py +++ b/src/aihawk/agent.py @@ -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 @@ -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]: @@ -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) diff --git a/src/aihawk/link.py b/src/aihawk/link.py index 9251ef46d..9662df73c 100644 --- a/src/aihawk/link.py +++ b/src/aihawk/link.py @@ -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 diff --git a/src/aihawk/runner.py b/src/aihawk/runner.py index a9edaf2ae..bc47e5b71 100644 --- a/src/aihawk/runner.py +++ b/src/aihawk/runner.py @@ -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 diff --git a/tests/test_agent_loop.py b/tests/test_agent_loop.py index ea735393e..417cd2570 100644 --- a/tests/test_agent_loop.py +++ b/tests/test_agent_loop.py @@ -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. diff --git a/tests/test_cli_surface.py b/tests/test_cli_surface.py index a699f65cd..ff7f97778 100644 --- a/tests/test_cli_surface.py +++ b/tests/test_cli_surface.py @@ -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 diff --git a/tests/test_key_isolation.py b/tests/test_key_isolation.py index 0442dde9d..e63543ea3 100644 --- a/tests/test_key_isolation.py +++ b/tests/test_key_isolation.py @@ -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 = {} diff --git a/tests/test_real_cli.py b/tests/test_real_cli.py index b11905b9a..615028df7 100644 --- a/tests/test_real_cli.py +++ b/tests/test_real_cli.py @@ -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. diff --git a/tests/test_ui_drive.py b/tests/test_ui_drive.py index ed17bb135..82ed41174 100644 --- a/tests/test_ui_drive.py +++ b/tests/test_ui_drive.py @@ -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. @@ -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 From 1fca9178f37b9e73f0d854b6ee0b732b7f3780b7 Mon Sep 17 00:00:00 2001 From: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:42:58 +0200 Subject: [PATCH 2/2] Bump to 0.11.1: this PR touches files under src/aihawk, which the version gate treats as shipped --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8147bffad..5ebddd5fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"