Skip to content

feat: stealth launch option + adopt the plugin's own running browser session (0.3.0) - #1

Merged
mabry1985 merged 4 commits into
mainfrom
feat/stealth-launch-and-self-daemon-guard
Sep 12, 2026
Merged

feat: stealth launch option + adopt the plugin's own running browser session (0.3.0)#1
mabry1985 merged 4 commits into
mainfrom
feat/stealth-launch-and-self-daemon-guard

Conversation

@mabry1985

Copy link
Copy Markdown
Member

Why

merchantAgent has never returned a live eBay sold comp. Two causes, both in this plugin's launch path:

  1. Google refuses the one-time sign-in. Chrome under CDP control carries navigator.webdriver, and Google's sign-in page answers "this browser or app may not be secure". The eBay account signs in through Google, so the profile never got signed in (zero eBay cookies from Aug 5 until this week).
  2. The daemon guard misfires on our own session. The ebay session daemon outlives the agent process. After a restart, or from a subagent that built its own tool set, ensure_session re-ran open --profile … --headed, the CLI printed ⚠ --profile, --headed ignored: daemon already running, and the plugin raised — telling the operator to agent-browser close --all, which kills the very session it was protecting (and every other plugin's browser). Hit twice in one evening.

What

  • stealth: false (manifest, ships off) → --args --disable-blink-features=AutomationControlled at launch. Same flag core's agent_browser plugin uses for its stealth option; nothing else changes. Documented in README and the skill as a Google-sign-in fix, not eBay evasion.
  • A clean launch writes <profile>/.protoagent-ebay-session.json with {session, profile, headed, stealth, launched_at}. When the CLI reports the daemon already running, the plugin reads it back: same options → adopt the session (log line, no error); different options → error naming exactly which option differs; no/garbled marker → the old foreign-daemon error. Every message says close --session ebay, never --all. close() removes the marker. No profile configured → no identity at stake → unchanged pass-through.
  • ebay_session_status adds the stealth instruction to next_step when not signed in and stealth is off; drops it once on.
  • SKILL.md: the "there is no Amazon data here" bullet was stale since 0.2.0 added amazon_price_check — it now says what the Amazon number is (asking, not sold). Also documents the new "different options" error.
  • Version 0.3.0 (manifest + pyproject).

Tests

126 host-free tests (was 114), ruff check + ruff format --check clean. New: foreign daemon still refused (and --all absent from the message), own session adopted across a fresh instance, option drift named (stealth), garbled marker = no marker, close clears the marker, no-profile pass-through, stealth flag present only when asked and still session-scoped, unwritable profile dir doesn't fail the launch, config → Browser wiring, manifest ships stealth off, session_status hint on/off/signed-in. The CLI stub now puts the warning on stderr with exit 0, which is what the real CLI does (verified live against agent-browser 0.27.1).

Not in this PR

  • No lifecycle hook to close the daemon on agent shutdown: a persistent daemon is now handled gracefully and keeps the signed-in window alive across restarts, which is the behavior we want.
  • The adopt decision trusts the marker; a daemon that died and was recreated under the same session name by something else would be adopted. Session names are per plugin, so this is accepted and documented in browser.py.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X1Mou2gxw6Wz9dLCmVocoL

…session

- `stealth: false` config -> `--args --disable-blink-features=AutomationControlled`
  at launch, the same flag core's agent_browser plugin uses for its stealth option
  and nothing more. Google refuses to sign an account in from a CDP-driven Chrome
  ("this browser or app may not be secure"), which is what stopped the one-time
  eBay sign-in for a Google-federated account.
- The "daemon already running" guard misfired on the plugin's OWN `ebay` session:
  the daemon outlives the agent process, so every fresh Browser instance (agent
  restart, subagent tool rebuild) raised and told the operator to `close --all`,
  killing the session it was protecting. A clean launch now records its options
  in `<profile>/.protoagent-ebay-session.json`; a later instance adopts a matching
  session, names the differing option otherwise, and only treats a marker-less
  daemon as foreign. Errors point at `close --session ebay`, never `--all`.
- `ebay_session_status` names the stealth fix when sign-in fails and stealth is off.
- SKILL.md no longer tells the agent "there is no Amazon data here" while
  `amazon_price_check` exists; it says what that number is (asking, not sold).
- 0.3.0. 126 host-free tests (was 114); ruff check + format clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1Mou2gxw6Wz9dLCmVocoL

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — WARN

code-review-structural · head 288302f8a2e3 · formal

Low-risk change: a single minor correctness issue in browser.py's close() path. The fix is straightforward — check the _run result (or wrap in try/except) before calling _clear_marker(). No panel disagreement; the lone finding was confirmed by the verifier. Verification was diff-only (direct file reads unavailable), so the structural pass was effectively skipped — the finding's grounding rests on the diff hunk alone.

Findings

Severity Location Finding Verified
🟡 minor browser.py close() unconditionally clears the launch marker after _run(self._cmd("close")) without checking whether the close succeeded; if the daemon is still running, t… confirmed
findings JSON (machine-readable)
[
  {
    "file": "browser.py",
    "line": 0,
    "severity": "minor",
    "category": "correctness",
    "claim": "close() unconditionally clears the launch marker after _run(self._cmd(\"close\")) without checking whether the close succeeded; if the daemon is still running, the next ensure_session() finds no marker and misidentifies our own session as a foreign one, raising a confusing 'foreign daemon' error.",
    "evidence": "def close(self) -> None:\n    _run(self._cmd(\"close\"), timeout=self.timeout_s)\n    self._clear_marker()\n    self._session_ready = False",
    "verdict": "confirmed",
    "note": "Code confirmed in PR diff: close() discards _run's result and unconditionally calls _clear_marker(). Since _run returns a result (not raises) per the ensure_session() pattern, a failed close still clears the marker, causing _adopt_running_session() to hit the marker-is-None branch on the next call."
  }
]

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promoting the WARN verdict for head 288302f8a2e3: all checks terminal-green, zero unresolved review threads. (approve-on-green)

Open findings carried by this approval — non-blocking, but they did not go away:

  • minor browser.py:0 — close() unconditionally clears the launch marker after _run(self._cmd("close")) without checking whether the close succeeded; if the daemon is still running, the next ensure_session() finds no marker

Approving a WARN does not resolve its findings (issue #22).

…arker

Review (adversarial pass + agent-browser 0.27.1 source) showed the first cut's model
of the CLI was wrong. A URL-less `open` with flags makes the CLI send TWO launches:
the full-options one, then the parsed command — an explicit launch carrying only
`headless`. The daemon reconciles launch options by hash, so it relaunches Chrome a
second time, on a throwaway temp profile. That was this plugin's launch step through
0.2.0: every window it ever opened was replaced within seconds by one on a profile
nobody was signed in to, which is why the profile dir never held a cookie. Verified
live by watching Chrome's --user-data-dir.

- ensure_session = `open about:blank --profile --headed [--args …] --session ebay`
  (one launch; the daemon starts / reuses / relaunches on its own).
- The same flags ride on every `open <url>`, core's proven pattern: a fresh process,
  a subagent's tool set, a config change or a dead daemon all converge on a browser
  with our options. Stealth now actually reaches the browser.
- The launch marker / adopt / session-list machinery is gone — the daemon's own
  reconciliation makes it unnecessary (and its "different options" branch would have
  told the operator to close a browser the daemon had just corrected).
- The CLI's "ignored: daemon already running" warning is client-side noise; no
  longer treated as an error. Nothing is written into the profile dir.
- `_as_bool` for headed/stealth ("false" from a form is False). Manifest no longer
  says `close --all`. `_StubBrowser` carries session/stealth; no getattr crutches.
- 127 host-free tests; ruff check + format clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1Mou2gxw6Wz9dLCmVocoL
@mabry1985

mabry1985 commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Redesign after review (head cb6ca34). The first cut assumed the CLI's "ignored: daemon already running" meant the flags were dropped. The source and a live check show the opposite and worse: a URL-less open with flags sends a full-options launch and then a second, option-less one, so the daemon relaunches Chrome on a temp profile — every window 0.2.0 ever opened was abandoned within seconds. Fix: launch via open about:blank with the flags and carry the flags on every open <url>; the daemon reconciles (reused / relaunch with new options / respawn). The marker + adopt + session-list machinery is gone, along with Vera's close() finding.

Live-verified on the running ebay daemon (agent-browser 0.27.1): one relaunch onto ~/.protoagent/ebay-profile with --disable-blink-features=AutomationControlled, navigator.webdriver === false, repeat launch reused (same Chrome pid), navigation with flags does not relaunch.

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — PASS

code-review-structural · head cb6ca3457731 · formal

Low-risk change: all four LLM finders and the structural pass found no defects. The prior-round minor finding about close() unconditionally clearing the launch marker was confirmed moot by the verifier — the code path it described no longer exists in the current head. No panel disagreement. No coverage gaps: the structural pass ran and returned clean. Nothing to fix.

No findings — the review came back clean.

findings JSON (machine-readable)
[]

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promoting the PASS verdict for head cb6ca3457731: all checks terminal-green, zero unresolved review threads. (approve-on-green)

- `_as_bool("")` returns the default: a blank `headed` field from a form must not
  mean headless (eBay refuses headless browsers).
- `ebay_session_status` hint says the relaunch follows the config reload, matching
  the manifest and skill.
- README/manifest: two instances that need different profiles need different
  `session` names — a session reconciles to whichever instance opened it last.
- Tidy one test assertion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1Mou2gxw6Wz9dLCmVocoL

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — PASS

code-review-structural · head e043044c247b · formal

Low-risk change: the only surviving finding is a nit-level docstring inconsistency in _as_bool where the documented falsy-string set omits "off". Fix first: add "off" to the docstring (or drop it from the code set if it was accidental). The panel did not disagree on any point. Verification confirmed the single finding without modification. No structural pass was skipped.

Findings

Severity Location Finding Verified
nit tools.py:183 _as_bool's docstring lists only "false"/"no"/"0" as the falsy strings, but the implementation also treats "off" as False, so the documented contract understate… confirmed
findings JSON (machine-readable)
[
  {
    "file": "tools.py",
    "line": 183,
    "severity": "nit",
    "category": "conventions",
    "claim": "_as_bool's docstring lists only \"false\"/\"no\"/\"0\" as the falsy strings, but the implementation also treats \"off\" as False, so the documented contract understates the accepted set (and no test covers \"off\").",
    "evidence": "Docstring: `\"\"\"A YAML/console flag: real bools pass through; the strings \"false\"/\"no\"/\"0\" mean False.\"\"\"` \u2014 but the code is `return text not in {\"false\", \"no\", \"0\", \"off\"}`, which also maps \"off\" to False; the test test_string_flags_from_a_settings_form_are_read_as_booleans exercises \"false\"/\"true\"/\"yes\"/\"\" but never \"off\".",
    "verdict": "confirmed",
    "note": "Verified in the PR diff: the docstring lists three falsy strings (\"false\", \"no\", \"0\") while the code's set includes a fourth (\"off\"); the test exercises \"false\", \"true\", \"yes\", and \"\" but never \"off\". All three sub-claims are accurate."
  }
]

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1Mou2gxw6Wz9dLCmVocoL
@mabry1985
mabry1985 merged commit de11592 into main Sep 12, 2026
@mabry1985
mabry1985 deleted the feat/stealth-launch-and-self-daemon-guard branch September 12, 2026 09:15

@protoreview protoreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QA panel review — PASS

code-review-structural · head 5238ea6224bc · formal

The change is low-risk: it adds a stealth launch option, refactors flag assembly into _launch_flags/_open_cmd, retires the now-obsolete _IGNORED_MARKER check in favour of the new daemon-reconciliation model, and updates the docs to match. No defects surfaced across the panel or the verifier's structural pass. The two prior-round items (a minor on browser.py close-marker handling and a nit on _as_bool's docstring) were both below the blocker/major threshold and do not carry forward. Nothing to fix before merge.

No findings — the review came back clean.

findings JSON (machine-readable)
[]

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