Skip to content

fix: unblock --url authoring serve and protect the runner link - #57

Open
abrichr wants to merge 3 commits into
mainfrom
fix/ledger-2026-10-10
Open

abrichr wants to merge 3 commits into
mainfrom
fix/ledger-2026-10-10

Conversation

@abrichr

@abrichr abrichr commented Oct 10, 2026

Copy link
Copy Markdown
Member

openadapt-agent serve --authoring --url <app> --headed exited at startup with "serve: Already running asyncio in this thread", so the stdio web authoring path couldn't be used. The --url browser now runs on its own thread, so the MCP server starts and every tool call reaches the browser. authoring connect --url now stops before it claims the one-use runner link when it can't open a browser you can sign in to. Before, it claimed the link and answered every command with "error".

Fixes

Problem Change Regression test Ref
serve --authoring --url <app> --headed exited with code 2 before the MCP server started. Playwright's sync API was started on the main thread, where it leaves an event loop marked as running, so anyio.run refused to start. Tool calls also run on MCP worker threads, and sync Playwright objects fail when another thread calls them. For --url, open_authoring_session launches the browser, builds the Flow authoring session, runs every call, and closes the browser on one daemon owner thread. It returns a ThreadOwnedSession that forwards attribute reads, calls, and writes to that thread. The mailbox Continue guard that blocks type_text still reaches the real session. If the session fails to start, its browser is closed. AuthoringBridge looks inside the wrapper when it checks for the coach-only stand-in. A Windows --url session that falls back to coach-only still refuses type and click with COACH_ONLY. tests/test_authoring.py: test_url_session_keeps_playwright_on_one_owner_thread_under_anyio, test_authoring_url_headed_serve_starts_and_dispatches_tools, test_real_url_session_serves_tools_under_anyio (real Chromium; skipped when Playwright or Chromium is missing, never downloads), test_url_session_construction_failure_closes_the_browser, test_url_coach_only_fallback_still_refuses_type_behind_owner_thread, test_url_session_refusal_stops_the_owner_thread; tests/test_mailbox.py: test_continue_guard_reaches_a_browser_owner_thread_session B076
authoring connect '<runner-link>' --url <app> without --headed couldn't open its browser, and that's the command docs/MAILBOX_CLI.md showed. The command claimed the one-use runner link anyway and asked the person to allow the chat. It then answered every command with a bare "error", so the person needed a new runner link and didn't know why. The same thing happened with --headed when the browser extra was missing. connect_mailbox refuses --url without --headed before it opens a browser or contacts the mailbox, and tells the person to add --headed. If the --url browser fails to open for any other reason, the command reports why before it claims the link. Both messages say the runner link wasn't used. A mailbox client that has a --url but no session answers as coach-only instead of "error". The Playwright example in docs/MAILBOX_CLI.md now passes --headed. tests/test_mailbox.py: test_connect_url_without_headed_refuses_before_claim, test_connect_url_refuses_before_claim_when_the_browser_cannot_open, test_url_client_without_a_session_is_coach_only_not_error, test_documented_url_connect_commands_pass_headed B085

Not fixed here

  • On Windows, authoring connect --url --headed without the browser extra still claims the runner link. The session falls back to coach-only before the mailbox sees an error, so commands get COACH_ONLY instead of a bare "error".
  • authoring connect doesn't close its browser session when it exits. Without an output folder, it writes recordings to runs/authoring in the current directory. Neither behavior changed here.
  • uv.lock locks openadapt-flow 1.34.0, which has no openadapt_flow.authoring module, so --authoring doesn't work in an environment built from the lock file. Installs with pip or uvx get a newer Flow that has the module.

How it was tested

  • Each regression test was run against main with only the test files copied in. The B076 tests fail there with "Already running asyncio in this thread", or because a browser that failed to start was left open. The B085 tests fail because main claims the runner link and doesn't raise an error, and because the docs example has no --headed. All of them pass on this branch.
  • test_url_coach_only_fallback_still_refuses_type_behind_owner_thread fails without the AuthoringBridge change. Without it, type returns "recorded": true even though nothing was typed. test_url_session_refusal_stops_the_owner_thread and test_continue_guard_reaches_a_browser_owner_thread_session don't reproduce a bug. They check that the new owner thread stops after a refusal and that the Continue guard still applies.
  • An MCP stdio client drove the real openadapt-agent serve --authoring --url http://127.0.0.1:<port>/ --headed command, with Chromium forced headless. observe returned the page's input and button, and start_record, click, and stop_record succeeded. On main, the same command exits 2 with "serve: Already running asyncio in this thread". The real-browser test also checks that observe returns a non-empty tree.
  • authoring connect --url --headed with a real browser and a mock mailbox completed Allow, observe, start_record, and halt. authoring connect '<runner-link>' --url <app> without --headed exits 2 with the new message and makes no network request.
  • Full suite: 222 passed on Python 3.12 (openadapt-flow 1.35.1, MCP 2.2.0). On Python 3.10 with the CI floor (openadapt-flow 1.26.0, MCP 1.28.0), 220 passed and 2 were skipped. On Python 3.11 with MCP 2.0.0, 221 passed and 1 was skipped.
  • ruff check src tests scripts passes. python -m build, scripts/check_release_artifacts.py, scripts/check_dist.py, and scripts/check_source_boundary.py --require-dist pass.

Before merging

  • This branch merges cleanly with open PR feat: add a four-outcome partner contract and a zero-flag sandbox #56, which also changes tests/conftest.py, src/openadapt_agent/mcp.py, README.md, and llms.txt. The combined suite passes (336 passed, 5 skipped).
  • Merging doesn't publish anything. To get the fixes to pip and uvx users, tag a normal openadapt-agent release through the reviewed release workflow.

🤖 Generated with Claude Code

abrichr and others added 3 commits October 10, 2026 00:06
`openadapt-agent serve --authoring --url <app> --headed` exited with
"serve: Already running asyncio in this thread" before the MCP server
started. Playwright's sync API was launched on the main thread before
anyio.run, and it leaves its own event loop marked running on the thread
that starts it. Even past startup, every MCP tool call ran on an anyio
worker thread, and sync Playwright objects fail when called from a thread
other than the one that created them.

open_authoring_session now launches the --url browser, builds the Flow
authoring session, serves every call, and closes the browser on one
dedicated daemon thread. Callers get a ThreadOwnedSession that forwards
attribute reads, method calls and attribute writes to that thread, so the
mailbox Continue guard that swaps out type_text still reaches the real
session. A session that fails to start now closes its browser instead of
leaving it running.

Tests cover the stdio serve path and the MCP server under anyio with a
fake browser bound to its launch thread, the Continue guard through the
owner thread, and (when Playwright Chromium is installed) a real headless
browser through the MCP tools.

Ledger: B076

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`openadapt-agent authoring connect '<runner-link>' --url <app>` without
--headed, which is the command docs/MAILBOX_CLI.md showed, could not open
its browser. The connect path swallowed that refusal, claimed the one-use
runner link anyway, asked the person to Allow the chat, and then answered
every command with a bare "error". The person needed a new runner link
and had no idea why. The same happened with --headed when the browser
extra was missing.

connect_mailbox now refuses --url without --headed before it opens a
browser or contacts the mailbox, and says to add --headed. Any other
failure to open the --url browser is reported with its reason before the
claim. Both messages say the runner link was not used. A mailbox client
that has a --url but no open session now answers as coach-only instead of
"error". The docs example now passes --headed, and a test checks every
documented `authoring connect --url` command does.

Ledger: B085

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When `serve --authoring --url` cannot pin a browser on Windows (no
--headed, or no browser extra), the session falls back to the coach-only
stand-in. The owner-thread change wraps that stand-in in a
ThreadOwnedSession, so AuthoringBridge's isinstance check no longer saw
it as coach-only. A `type` call then answered {"status": "ok",
"recorded": true} although nothing was typed or recorded.

AuthoringBridge now looks through the owner-thread wrapper when it
decides whether a session is coach-only, so `type` and `click` refuse
with COACH_ONLY again. The new test fails without this change.

Ledger: B076

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant