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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ claude mcp add openadapt-authoring -- \
serve --authoring
```

`--bundles` is omitted. Probe tools are `observe`, `start_record`, `click`, and `halt`. This process stays stdio. Pass `--url` to pin a fresh Playwright Chromium with empty cookies. Windows native, Citrix, and RDP stay coach-only here, as does any macOS or Linux session without a unique frontmost window.
`--bundles` is omitted. Probe tools are `observe`, `start_record`, `click`, and `halt`. This process stays stdio. Pass `--url --headed` to pin a fresh Playwright Chromium with empty cookies. Sign in in that window with `pause_for_input`; `continue_input` records it. `--url` is not the Chrome you already signed into, and it does not attach over CDP. Omit `--url` after you sign in in Chrome, and the unique frontmost window pins on macOS (no DOM identity). Windows native, Citrix, and RDP stay coach-only here, as does any macOS or Linux session without a unique frontmost window.

Private customer bundles still use `--bundles`. Those stay on the operator's disk and are never shipped here.

Expand Down
23 changes: 18 additions & 5 deletions src/openadapt_agent/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,29 +305,42 @@ def pin_local_backend(
not speak that protocol from this MIT package. Otherwise:

- ``url`` launches Playwright Chromium with empty cookies (no debug-port
attach, not the person's already-logged-in Chrome).
attach, not the person's already-logged-in Chrome). ``headed`` must be
true so a person can sign in in that window via ``pause_for_input``.
- Windows native / RDP / Citrix → coach-only; never the in-guest Windows
agent HTTP helper.
- macOS / Linux → unique frontmost window via Flow backends when those
constructors are importable (F1). Non-unique Linux titles are coach-only.
Omit ``url`` after the person signs in in Chrome if DOM identity is not
required.
"""

if backend is not None:
kind = backend_kind or "web"
return backend, kind, None
plat = platform or sys.platform
if url:
if not headed:
raise AuthoringError(
"--url launches Playwright Chromium with empty cookies, not "
"the Chrome window you already signed into. Pass --headed and "
"pause_for_input so a person can sign in in that window, then "
"continue_input. Or pass no --url and pin the unique "
"frontmost Chrome window after they sign in (macOS; no DOM "
"identity)."
)
return _pin_web(url, headed=headed)
if plat == "win32" or plat.startswith("win"):
return None, "windows", None
native = _try_pin_native(plat)
if native is not None:
return native
raise AuthoringError(
"stdio --authoring needs a locally pinned window: pass --url for "
"Playwright Chromium with empty cookies, or run Desktop so overlay "
"stays single-owner. Native pin uses Flow backends after a unique "
"frontmost window (openadapt_flow.authoring / F1)"
"stdio --authoring needs a locally pinned window: pass --url --headed "
"for Playwright Chromium with empty cookies (pause_for_input to sign "
"in there), omit --url after a unique frontmost Chrome window "
"(macOS; no DOM identity), or run Desktop so overlay stays "
"single-owner. Debug-port attach is out of v1."
)


Expand Down
11 changes: 7 additions & 4 deletions src/openadapt_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,13 @@ def build_parser() -> argparse.ArgumentParser:
help=(
"Register first-demo authoring tools over local stdio: observe, "
"start_record, click, halt. Local Claude Code path is the first "
"authoring UI. Pass --url to pin Playwright Chromium with empty "
"cookies. Local stdio may also type through the recorder; hosted "
"MCP remains pause-only. Does not enable run tools. This process "
"stays stdio and must not be served over HTTP."
"authoring UI. Pass --url --headed to pin Playwright Chromium "
"with empty cookies; pause_for_input is how a person signs in "
"there. --url is not the Chrome window you already signed into. "
"Omit --url to pin a unique frontmost Chrome window after login "
"(macOS; no DOM identity). Local stdio may also type through the "
"recorder; hosted MCP remains pause-only. Does not enable run "
"tools. This process stays stdio and must not be served over HTTP."
),
)
p.add_argument(
Expand Down
48 changes: 48 additions & 0 deletions tests/test_authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ def test_authoring_sources_stay_stdio_without_http_listener():
assert "FastAPI" not in text
assert "win_agent" not in text
assert "parallels_vm" not in text
assert "connect_over_cdp" not in text


def test_observe_wire_omits_extra_keys_and_invalid_node_ids():
Expand Down Expand Up @@ -652,6 +653,53 @@ def test_flow_shaped_session_maps_f1_signatures_and_stale_node():
assert session.calls[-1][2] == "authoring"


def test_login_pause_records_observed_and_compile_needs_human_admit():
session = FlowShapedSession()
bridge = AuthoringBridge(session, out_dir="/tmp/authoring-login")
assert bridge.dispatch("start_record", {}) == {"status": "recording"}
paused = bridge.dispatch(
"pause_for_input",
{"node_id": "n_9f2c001a", "param": "password", "secret": True},
)
assert paused == {"status": "paused", "param": "password", "secret": True}
assert session.typed_via_backend == []
continued = bridge.dispatch("continue_input", {})
assert continued == {"recorded": True, "param": "password"}
assert session.observed_events == [
{"kind": "type", "param": "password", "secret": True}
]
compiled = bridge.dispatch("compile", {})
assert compiled["status"] == "needs_human_admit"
assert "VERIFIED" not in json.dumps(compiled)


def test_url_without_headed_fails_loud_empty_cookies():
with pytest.raises(AuthoringError, match="empty cookies") as exc_info:
pin_local_backend(url="https://example.invalid/app", headed=False)
message = str(exc_info.value)
assert "already signed into" in message
assert "pause_for_input" in message
assert "no --url" in message


def test_headed_url_pins_playwright_not_cdp(monkeypatch):
launched: dict = {}

def fake_pin_web(url, *, headed):
launched["url"] = url
launched["headed"] = headed
return object(), "web", lambda: None

monkeypatch.setattr("openadapt_agent.authoring._pin_web", fake_pin_web)
backend, kind, close = pin_local_backend(
url="https://example.invalid/app", headed=True
)
assert kind == "web"
assert launched == {"url": "https://example.invalid/app", "headed": True}
assert callable(close)
assert backend is not None


def test_coach_hint_and_bind_status_are_local():
bridge = AuthoringBridge(FakeAuthoringSession())
with pytest.raises(AuthoringError, match="PHI"):
Expand Down
3 changes: 3 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ def test_authoring_help_says_run_tools_stay_off_and_stdio_only(capsys):
assert "--authoring" in out
assert "Does not enable run tools" in out
assert "HTTP" in out
assert "empty cookies" in out
assert "pause_for_input" in out
assert "already signed into" in out


def test_authoring_does_not_imply_allow_run_without_bundles(capsys):
Expand Down