- name
- debug-store
- description
- Developer-only skill for poking at vibe-seller stores from outside the task system — inspect the SQLite DB, drive a store's browser-use wrapper directly via the CDP proxy, manage VIBE_TASK_ID for stable sessions, recover wedged daemons, find seller SKUs, and authenticate without the JWT cookie path. Load when the user says 'debug a store', 'why is store X broken', 'check the wrapper for store X', 'I need to drive Ziniao directly without creating a task', or anything that touches the runtime store layer outside the normal task → agent flow.
# Debug Store Infrastructure (developer-only)
This is the inside-out view of vibe-seller stores: how to inspect and
drive the runtime — the database, the browser-use wrapper, the CDP
proxy, the per-store Ziniao session — *without* going through the task
system, the web UI, or the JWT-cookie auth wall.
This is for the platform builder, not for end-user agents running tasks.
For end-user/agent flows (running campaigns, etc.), see `amazon-ads` and
`new-product-launch`.
## What you can do without a task
The vibe-seller per-store wrapper is just a shell script that talks to
a long-running CDP-mux proxy. The wrapper auto-starts the proxy and
the browser via an internal API call (with a baked-in `ai_bot` JWT,
so you don't need user creds). All you need is:
1. A fresh `VIBE_TASK_ID` env var (lowercase UUID; first 8 chars must
be hex).
2. The store slug (look it up in `~/.vibe-seller/data/vibe_seller.db` —
`stores.name`, then slugify; or just list `~/.vibe-seller/bin/` for
the directories that actually exist).
Then `~/.vibe-seller/bin/<slug>/browser-use open <url>` works.
## DB cheatsheet
```bash
DB=~/.vibe-seller/data/vibe_seller.db
sqlite3 "$DB" .tables
sqlite3 "$DB" "SELECT id, name, browser_backend, platforms, countries FROM stores;"
sqlite3 "$DB" ".schema stores"
sqlite3 "$DB" "SELECT id, username, email, role FROM users;"
sqlite3 "$DB" "SELECT store_id, status, current_platform, current_country,
active_tab_count, cdp_port, chrome_pid
FROM browser_sessions;"
sqlite3 "$DB" "SELECT id, status, substr(result,1,120), substr(error,1,120)
FROM tasks WHERE created_at > datetime('now','-1 hour')
ORDER BY created_at DESC LIMIT 10;"
```
The `stores` table has no `slug` column — the wrapper directory
(`~/.vibe-seller/bin/<slug>/`) is where the actual usable name lives.
`name` in DB is the user-facing label and may differ from the slug
when names contain spaces / non-ascii. Match by ID when scripting.
The user table is seeded on first boot with two entries:
- a human admin whose username/email/password come from the
`ADMIN_USERNAME` / `ADMIN_EMAIL` / `ADMIN_PASSWORD` env vars
(defaults: `admin` / `admin@vibe-seller.local` / `admin`). After
the first login the user can change these, so don't assume
the seeded values are still current — read them from the DB.
- `ai_bot` (id `00000000-0000-0000-0000-000000000002`, email
`ai@vibe-seller.local`, `password_hash='disabled'`) — its JWT is
baked into the wrapper for `browser/start` API calls. **Interactive
login for `role='ai_bot'` is rejected by `auth.py`**, so don't try
to use this account for HTTP API calls. Use it only as the
internal-token subject (which the wrapper already does for you) or
follow Option A below to create your own admin user.
User-created accounts may also exist alongside these two seeds.
`password_hash` is bcrypt; the literal `disabled` means the account
cannot log in regardless of role.
## Wrapper architecture in 30 seconds
Each store has a generated wrapper at `~/.vibe-seller/bin/<slug>/browser-use`.
The wrapper:
1. Resolves `SESSION` from `VIBE_TASK_ID`:
- With `VIBE_TASK_ID` set → `<slug>-<first 8 hex>`
- Without → `<slug>` (the global session)
2. Validates the session name against `^<slug>(-aux|-[0-9a-fA-F]{8})?$`.
3. Auto-starts the CDP proxy + Ziniao Chrome by calling
`POST http://127.0.0.1:7777/api/stores/<store_id>/browser/start`
with a hardcoded `ai_bot` JWT. Polls until **that store's** proxy port
responds — ports are allocated per store from `_BASE_PROXY_PORT`
(9222), so real stores sit on 9223, 9226, 9227… Read the actual port
out of the wrapper (`grep -o '127\.0\.0\.1:9[0-9]\{3\}'`) or the
`browser_sessions` row; never assume 9222.
4. Exports `BU_CDP_WS=ws://127.0.0.1:<proxy_port>/client-<VIBE_TASK_ID>`
so the browser-use daemon connects through the mux proxy as a unique
client. (browser-use 0.13 dropped `--cdp-url` for this env var — the
wrapper *rejects* `--cdp-url`, see the blocked-flags list above.)
5. Execs the real `browser-use` binary with the rest of the args.
The wrapper blocks `--profile`, `--cdp-url`, and `--connect` flags
because those break the mux. `--headed` is also blocked (managed by
the wrapper). For non-seller-center pages, use
`--session <slug>-aux`. **Behavior depends on backend:**
- **Wrapper format v4** — every session carries an EXPLICIT
`BU_CDP_WS` (an endpoint-less daemon ambiently attached to a
DIFFERENT store's browser — observed live; that class is now
unrepresentable). Ziniao stores: `-aux` is the store's DEDICATED
login-less Chromium, lazily started via
`POST /api/stores/{id}/browser/aux/start` (own CDPMuxProxy, own
`downloads/{slug}-aux/` dir, NO seller login — never use it for
seller central). Chrome stores: aux is a stable `client-aux` on
the main proxy. If an aux session shows another store's account,
the wrapper predates v3/v4 — restart the server to regenerate.
## VIBE_TASK_ID — the recurring footgun
A fresh `VIBE_TASK_ID` per real task is what the runtime gives every
task agent. Outside the task system, **rotate it manually after**:
- Every long pause (> 15 min idle, especially if the user resumed
after sleeping for hours).
- Any time you see `Error: Session '<slug>-<8hex>' is already running
with different config.` — means the daemon for that ID has stale
CDP config.
- Any time `browser-use sessions` shows the daemon for your ID with
`CONFIG=?` (TTY-detection failure mode).
- Any time eval calls return `TimeoutError: timed out` or
`Client is stopping` — daemon has wedged.
Rotate:
```bash
NEW="$(uuidgen | tr '[:upper:]' '[:lower:]')"
echo "export VIBE_TASK_ID='$NEW'" > /tmp/vs_session_env.sh
. /tmp/vs_session_env.sh
~/.vibe-seller/bin/<slug>/browser-use sessions
~/.vibe-seller/bin/<slug>/browser-use open <some-url>
```
> **A rotated `VIBE_TASK_ID` gets REAPED within 5 minutes — do not use
> one for manual driving.** `VIBE_TASK_ID=<uuid>` resolves the session to
> `<slug>-<8hex>`, which is exactly the shape
> `daemon_reaper.task_prefix_for_bu_name()` reads as *"owned by a task"*.
> There is no task row for a hand-made UUID, so the 5-minute sweep
> classifies the daemon as orphaned and kills it. The next `js()` in your
> heredoc then dies with a bare, undiagnosable traceback —
> `FileNotFoundError: [Errno 2] No such file or directory` out of
> `_ipc.connect` — because `ensure_daemon()` runs **once** before
> `exec(code)` and nothing re-checks the socket mid-script. Confirm with:
> `grep -i reaped logs/backend_7777.log`
> → `Reaped 1 orphaned browser-use daemon(s): [(<pid>, '<8hex>')]`.
>
> For manual driving use a session the reaper **skips** — one with no
> task suffix: unset `VIBE_TASK_ID` entirely (session `<slug>`, the
> global one) or pass `--session <slug>-aux`. Rotation advice above
> applies to a *wedged* daemon inside a real task, not to your shell.
## Reviving a wedged daemon
```bash
# 1. List daemons to find the wedged one
~/.vibe-seller/bin/<slug>/browser-use sessions
# 2. Force-kill (the wrapper's `close` subcommand may itself hang)
pkill -9 -f "skill_cli.daemon.*<slug>-<8hex>"
# 3. Rotate VIBE_TASK_ID and re-open
NEW="$(uuidgen | tr '[:upper:]' '[:lower:]')"
export VIBE_TASK_ID="$NEW"
~/.vibe-seller/bin/<slug>/browser-use open about:blank
```
The Ziniao Chrome process and the CDP mux proxy survive daemon kills —
they're per-store, not per-task. Don't kill them unless you've tried
everything else.
To verify the CDP proxy is actually responding:
```bash
# This store's mux port — NOT a fixed 9222 (allocated per store).
PORT=$(grep -oE '127\.0\.0\.1:9[0-9]{3}' ~/.vibe-seller/bin/<slug>/browser-use | head -1 | cut -d: -f2)
curl -sf -m 2 --noproxy '*' "http://127.0.0.1:$PORT/json/version"
ps aux | grep -E "ziniaobrowser" | grep -v grep
```
`--noproxy '*'` matters: with ClashX/etc. exported, curl sends the
loopback request to the proxy and you get an empty body instead of a
connection error — which reads as "the browser is up but broken".
The mux proxy is an asyncio server **inside** the FastAPI process, so
`grep cdp_mux_proxy` in `ps` finds nothing even when it is running.
**Wedge signature:** the port accepts the TCP connection and then never
answers (curl hangs to `-m` timeout, HTTP 000). Distinct from a clean
"connection refused" — see
[docs/ziniao-concurrency.md](../../../docs/ziniao-concurrency.md) runbook.
## Skipping JWT-cookie auth
The vibe-seller HTTP API (`/api/tasks`, `/api/stores`, etc.) gates
everything on a JWT cookie. From a Claude Code shell, you don't have
that cookie. Two ways to work around without touching the user's
admin password:
### Option A — reusable `taskbot_debug` account, login via API, then deactivate
Use the single stable `taskbot_debug` account for every debug session.
The flow below is idempotent: it creates the account on first use,
reactivates + rotates the password on every later use, and deactivates
it when you're done.
```bash
# 1. Generate password + bcrypt hash
PW=$(python3 -c "import secrets;print('tmp_'+secrets.token_hex(8))")
HASH=$(./.venv/bin/python3 -c "from app.password import hash_password;print(hash_password('$PW'))") # run from repo root
# 2. Upsert the stable debug user (admin role, fixed id + username)
DBG_UID="taskbot-debug"
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
sqlite3 ~/.vibe-seller/data/vibe_seller.db \
"INSERT INTO users (id, username, email, password_hash, role, is_active,
plan_mode_default, debug_mode, default_profile_id,
created_at, updated_at)
VALUES ('$DBG_UID','taskbot_debug',NULL,'$HASH','admin',1,1,0,'default',
'$NOW','$NOW')
ON CONFLICT(id) DO UPDATE SET
password_hash='$HASH', is_active=1, updated_at='$NOW';"
# 3. Login → cookie
curl -s -c /tmp/vs_cookie.txt -H 'Content-Type: application/json' \
-X POST http://localhost:7777/api/auth/login \
-d "{\"identifier\":\"taskbot_debug\",\"password\":\"$PW\"}"
# 4. Use the cookie for any API call
curl -s -b /tmp/vs_cookie.txt http://localhost:7777/api/stores
# 5. Cleanup before ending the session
sqlite3 ~/.vibe-seller/data/vibe_seller.db \
"UPDATE users SET is_active=0 WHERE id='$DBG_UID';"
```
`is_active=0` blocks future logins for this account. The
`tasks.created_by` column uses FK = restrict, so the row itself can
never be deleted once it owns tasks — that's fine, it's one stable
row across all sessions. If you forget step 5 the account stays
dormant with a throwaway password; the next session rotates it.
### Option B — drive the wrapper directly (no API auth needed)
If your only goal is to drive a store's browser, you don't need the
HTTP API at all. The wrapper itself authenticates the
`browser/start` call with the `ai_bot` JWT baked into the script.
Just rotate `VIBE_TASK_ID` and call `browser-use open <url>`.
This is the right choice when:
- You're inspecting a single store from outside (debugging, verifying
a workflow before turning it into a task).
- You don't need the per-task workspace isolation.
- You're OK with sharing the global `<slug>` session (or pinning a
per-shell `<slug>-<8hex>` session via `VIBE_TASK_ID`).
This is the WRONG choice when:
- You want isolation across concurrent runs (use real tasks).
- You need the agent's MCP tools (`vibe_seller_write_workspace_file`,
catalog injection, etc.) — those only exist inside a real task.
## Calling the HTTP API
Once you have a cookie file (Option A) — assume `COOKIE=/tmp/vs_cookie.txt`
and `BASE=http://localhost:7777` below — every authenticated route is
just `curl -b "$COOKIE" "$BASE/<path>"`. Use `-c "$COOKIE"` on the
login call (writes), `-b` everywhere else (reads).
The API surface (full list in `docs/api.md`) breaks into a few groups:
| Group | Common routes | Use for |
|---|---|---|
| Auth | `POST /api/auth/login`, `GET /api/auth/me`, `POST /api/auth/logout` | Cookie lifecycle |
| Stores | `GET /api/stores`, `GET /api/stores/<id>`, `POST /api/stores/<id>/browser/start` (`?force=1`), `POST /api/stores/<id>/browser/aux/start` | Store metadata + per-store browser lifecycle |
| Tasks | `POST /api/tasks`, `GET /api/tasks`, `GET /api/tasks/<id>`, `POST /api/tasks/<id>/messages`, `POST /api/tasks/<id>/stop` | Create / list / inspect / message / stop tasks |
| Schedules | `GET /api/schedules`, `POST /api/schedules`, `POST /api/schedules/<id>/run` | Cron-style routines |
| Events | `GET /api/events/stream` (SSE), `GET /api/events/recent` | Live + historical event feed |
> **There is no `browser/stop` route** — stopping is internal only
> (`BrowserManager.stop_session` / `aux_browser.stop_aux`, called by store
> deletion and the idle sweeper). To stop one store's env by hand, send
> Ziniao's own per-store `stopBrowser` with that store's `browser_oauth`;
> never kill the shared Ziniao client, which would destroy every other
> store's live browser (see
Voir sur GitHub