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
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,35 @@ plugins:
ebay:
profile: ~/.protoagent/ebay-profile # REQUIRED — this is what keeps you signed in
headed: true
stealth: false # true if your eBay account signs in through Google (below)
domain: www.ebay.com # www.ebay.co.uk, www.ebay.de, …
amazon_domain: www.amazon.com
```

Then ask the agent to run `ebay_session_status`, and **sign in once** in the window that
opens. The profile persists it.

> `profile` and `headed` are **daemon-level** launch options in `agent-browser`. If a browser
> daemon is already running under different options the CLI ignores them silently — so the
> plugin raises instead of browsing as the wrong identity. `agent-browser close --all`, then
> retry.
### Signing in through Google

Chrome sets `navigator.webdriver` when it is driven over CDP, and Google refuses to sign an
account in from such a browser ("This browser or app may not be secure"). If your eBay account
signs in through Google, set `stealth: true`: the browser launches with
`--disable-blink-features=AutomationControlled` — the same flag protoAgent's core browser plugin
uses for its stealth option — and nothing else changes. The browser still identifies as Chrome
and runs at a human pace; this is not an attempt to defeat eBay's own checks. An eBay password
or passkey login needs none of this.

> `profile`, `headed` and `stealth` are **daemon-level** launch options in `agent-browser`. The
> plugin sends them with every page open and the daemon reconciles them against the running
> browser: unchanged → reused; changed → Chrome relaunches with the new options on the same
> profile (a sign-in survives); daemon gone → one is respawned with them. So a fresh agent
> process, a subagent with its own tool set, or a config change all converge on the right
> browser with nothing to close by hand. (Through 0.2.0 the launch step was a URL-less `open`,
> which the CLI turns into a second, option-less launch — every window it opened was replaced
> within seconds by one on a throwaway profile. Fixed in 0.3.0.) If you ever do need to reset
> the browser: `agent-browser close --session ebay`, never `close --all`, which also kills
> every other plugin's browser. Two instances that need *different* profiles need different
> `session` names too — one session reconciles to whichever instance opened it last.

## Tools

Expand Down
78 changes: 47 additions & 31 deletions browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@
each other (the event bus is the only inter-plugin channel), and this keeps the test suite
host-free — every call goes through :func:`_run`, which tests replace wholesale.

The one non-obvious thing about the CLI: **launch options are daemon-level, not
per-command.** ``open --profile X`` on an already-running daemon prints
``⚠ --profile, --headed ignored: daemon already running`` and proceeds with whatever
profile that daemon started under. Silently accepting that would mean browsing as the wrong
identity — logged out, or worse, as some other account — so :meth:`Browser.ensure_session`
surfaces it as a real error instead. The operator either closes the daemon or points this
plugin at the profile already in use.
**Launch options are daemon-level, and how they are sent matters.** ``--profile``,
``--headed`` and ``--args`` describe the Chrome the daemon runs. Given on an ``open <url>``,
the CLI sends one full-options *launch* ahead of the navigation and the daemon reconciles it
against the running browser: same options → reused; different → Chrome is relaunched with the
new ones (on the same profile, so a sign-in survives); no daemon → one is started with them.
So this wrapper rides the flags on EVERY ``open <url>``. A fresh agent process, a subagent's
own tool set, a config change, a daemon that died — all converge on a browser with our
options and no bookkeeping.

What it must never do is a URL-less ``open`` with flags. That parses to an explicit launch
carrying only ``headless``, sent right after the full-options one, and the daemon dutifully
relaunches Chrome a second time — on a throwaway temp profile. That was this plugin's launch
step through 0.2.0: every "signed-in" window it ever opened was abandoned within seconds for
one on a profile nobody was signed in to, which is why the profile dir never held a cookie.
(The CLI's ``⚠ … ignored: daemon already running`` warning is printed client-side whenever a
daemon exists and flags were given; it is noise, not a signal — the daemon applied them.)
Verified live against agent-browser 0.27.1 by watching Chrome's ``--user-data-dir``.
"""

from __future__ import annotations
Expand All @@ -25,8 +35,10 @@

log = logging.getLogger("protoagent.plugins.ebay")

#: The CLI prints this when launch flags were dropped because a daemon was already up.
_IGNORED_MARKER = "ignored: daemon already running"
#: Chrome flag that clears ``navigator.webdriver``, which Chrome sets under CDP control and
#: which Google's sign-in refuses ("this browser or app may not be secure"). The same flag
#: protoAgent's core browser plugin uses for its ``stealth`` option — and nothing more.
_STEALTH_ARGS = "--disable-blink-features=AutomationControlled"


class BrowserError(RuntimeError):
Expand Down Expand Up @@ -55,8 +67,9 @@ class Browser:
"""Drives one named ``agent-browser`` session.

``min_interval_s`` paces navigations. This is politeness, not evasion — it keeps a
research loop from hammering a site faster than a person would. Nothing here tries to
look like something it isn't.
research loop from hammering a site faster than a person would. ``stealth`` drops the
one automation flag that blocks Google's sign-in page; the browser still identifies
itself as Chrome and nothing here tries to look like something it isn't.
"""

def __init__(
Expand All @@ -66,13 +79,15 @@ def __init__(
session: str = "ebay",
profile: str = "",
headed: bool = True,
stealth: bool = False,
timeout_s: float = 60.0,
min_interval_s: float = 1.5,
):
self.binary = binary
self.session = session
self.profile = profile
self.headed = headed
self.stealth = stealth
self.timeout_s = timeout_s
self.min_interval_s = min_interval_s
self._last_nav = 0.0
Expand All @@ -97,35 +112,34 @@ def _pace(self) -> None:
time.sleep(self.min_interval_s - gap)
self._last_nav = time.monotonic()

def _launch_flags(self) -> list[str]:
"""The daemon-level options, sent with every ``open <url>`` (module docstring)."""
flags: list[str] = []
if self.profile:
flags += ["--profile", self.profile]
if self.headed:
flags += ["--headed"]
if self.stealth:
flags += ["--args", _STEALTH_ARGS]
return flags

def _open_cmd(self, url: str) -> list[str]:
return [self.binary, "open", url, *self._launch_flags(), "--session", self.session]

# ── session ─────────────────────────────────────────────────────────────────
def ensure_session(self) -> None:
"""Launch the browser once, with our profile actually applied.
"""Bring the browser up with our options, once per instance.

Raises if the CLI dropped the launch flags because another daemon already owns the
browser: the whole point of the profile is the logged-in eBay session, so quietly
continuing without it would produce confidently wrong answers from a logged-out or
unrelated identity.
``open about:blank`` with the flags: the daemon starts, reuses, or relaunches as
needed (module docstring). Never a URL-less ``open`` — that is the double launch.
"""
if self._session_ready:
return
if reason := self.available():
raise BrowserError(reason)
args = [self.binary, "open"]
if self.profile:
args += ["--profile", self.profile]
if self.headed:
args += ["--headed"]
args += ["--session", self.session]
res = _run(args, timeout=self.timeout_s)
res = _run(self._open_cmd("about:blank"), timeout=self.timeout_s)
if not res.ok:
raise BrowserError(f"could not start the browser: {(res.stderr or res.stdout).strip()}")
if self.profile and _IGNORED_MARKER in (res.stdout + res.stderr):
raise BrowserError(
"a browser daemon is already running, so this plugin's profile "
f"({self.profile}) was ignored — it would be browsing as whatever identity that "
"daemon started under, not your signed-in eBay session. Run `agent-browser close "
"--all` and retry, or set ebay.profile to the profile already in use."
)
self._session_ready = True

def close(self) -> None:
Expand All @@ -136,7 +150,9 @@ def close(self) -> None:
def open(self, url: str) -> None:
self.ensure_session()
self._pace()
res = _run(self._cmd("open", url), timeout=self.timeout_s)
# Flags on every navigation: a daemon that died (an operator's `close`, a newer
# CLI's idle timeout) respawns with our profile instead of a blank one.
res = _run(self._open_cmd(url), timeout=self.timeout_s)
if not res.ok:
raise BrowserError(f"could not open {url}: {(res.stderr or res.stdout).strip()}")

Expand Down
19 changes: 15 additions & 4 deletions protoagent.plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id: ebay
name: Marketplace prices (eBay + Amazon, via browser)
version: 0.2.0
version: 0.3.0
description: >-
Price research across eBay and Amazon — what an item actually SOLD for on eBay, what it
is being asked on both, and where today's price sits against what we've recorded before.
Expand All @@ -25,15 +25,26 @@ config:
# The agent-browser CLI on PATH (override for a pinned path).
binary: agent-browser
# Named browser session, so eBay work doesn't fight another plugin over the default one.
# Two instances that need DIFFERENT profiles need different session names too — a session
# reconciles to whichever instance opened it last.
session: ebay
# Persistent browser-profile dir — this is what keeps you signed in to eBay between runs.
# Blank means no profile, which means signing in every session and losing the sold view.
# NOTE: profile/headed are DAEMON-level launch options. If a browser daemon is already
# running under different options the CLI silently ignores these, so the plugin raises
# instead of browsing as the wrong identity. `agent-browser close --all` then retry.
# profile/headed/stealth are DAEMON-level launch options; the plugin sends them with every
# page open and the daemon reconciles: unchanged → reused, changed → the browser relaunches
# with the new ones (same profile, so the sign-in survives), gone → respawned with them.
profile: ""
# eBay refuses headless browsers with an error page. Leave this on.
headed: true
# Drop the `navigator.webdriver` automation flag Chrome sets under CDP control
# (`--disable-blink-features=AutomationControlled`, the same flag protoAgent's core browser
# plugin uses for its stealth option, and nothing more). Google refuses to sign an account
# in from a browser carrying that flag ("this browser or app may not be secure"), so an eBay
# account that signs in THROUGH GOOGLE needs this on for the one-time sign-in. It is not an
# attempt to defeat eBay's own checks: the browser still identifies as Chrome and runs at a
# human pace. Takes effect on the next page open after the config reload — the browser
# relaunches itself with it.
stealth: false
# Marketplace domains — www.ebay.co.uk / www.amazon.co.uk … for a non-US market.
domain: www.ebay.com
amazon_domain: www.amazon.com
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ebay-plugin"
version = "0.2.0"
version = "0.3.0"
description = "Marketplace price research (eBay + Amazon) + seller economics for protoAgent, via a browser session"
requires-python = ">=3.11"

Expand Down
13 changes: 9 additions & 4 deletions skills/ebay-pricing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ paper over the second as "I didn't find anything" — that turns a plugin bug in
market claim.

- **sign-in wall** → run `ebay_session_status`, tell the operator to sign in once in the
browser window. It persists after that.
browser window. It persists after that. If the operator reports Google refusing the sign-in
as an insecure browser, that is config, not something to click past: `ebay.stealth: true`,
then `ebay_session_status` again once the config has reloaded — the browser relaunches
with it.
- **challenge page** → eBay wants a human interaction; ask the operator to complete it. If
it recurs, suggest raising `ebay.min_interval_s`.
- **couldn't find the results list** → eBay changed its markup. Run `ebay_page_probe` on the
Expand All @@ -56,9 +59,11 @@ market claim.

- **Sell-through rate.** Sold comps show what sold, never how many *didn't*. A high median
on a slow-moving item is not a good price.
- **Amazon or other marketplaces.** Amazon's Product Advertising API is deprecated and
returns 403; its replacement needs an Associates account with qualifying sales. There is
no Amazon data here. Say that plainly rather than reasoning about Amazon from memory.
- **Amazon sold prices or history.** `amazon_price_check` returns what Amazon is *asking*
right now (Buy Box, sponsored rows excluded) — Amazon publishes no sold data and no price
history, and `price_history` holds only what this plugin has recorded since it was enabled.
Use the Amazon number as a ceiling a new buyer compares against, never as a comp, and say
which it is rather than reasoning about Amazon from memory.
- **Anything older than eBay shows.** The sold view reaches back a limited window.

Say which of these applies when it bears on the answer. A caveat stated is worth more than a
Expand Down
Loading
Loading