workstation: per-user playwright browser MCP for all users, reproducible from git
Viktor asked that the playwright browser MCP be available for every devvm user
in every directory, with each user running their own server and multiple
concurrent sessions per user.
Before this, playwright was hand-set-up per user (~/.config/systemd/user/
playwright-mcp.service on 8931/8932/8933) and only wizard was actually wired —
emo's and anca's servers ran but their ~/.claude.json had no playwright entry,
so their Claude never connected. None of it was reproducible from git (units,
refresh script, and the Vault snapshot token lived only in user homes), so a
devvm rebuild would silently lose it.
This makes it reproducible and fixes the unwired users:
- roster_engine.py: sticky per-user PLAYWRIGHT_PORT (PLAYWRIGHT_BASE_PORT=8931,
allocated for every roster user incl. the admin), emitted in the derive JSON.
- scripts/workstation/playwright/: system-level TEMPLATE units
(playwright-mcp@.service + playwright-snapshot-refresh@.{service,timer},
User=%i — system manager, so no systemd --user / linger) + the refresh script.
@playwright/mcp pinned to 0.0.76 (avoids the @latest silent-fleet-roll
footgun, same rationale as T3_PIN).
- setup-devvm.sh: install the templates + script (9e); stage the chrome-service
snapshot bearer token from Vault to a root file (8c) — the hourly root
reconcile has no Vault token, mirrors the Claude OAuth staging in 8a.
- t3-provision-users.sh: install_playwright() (ALL tiers incl. admin) writes
PLAYWRIGHT_PORT, seeds the token if-absent, wires the user-scope ~/.claude.json
by running `claude mcp add` AS the user (clobber-proof + if-absent, so it fixes
existing/new/admin without rewriting a populated config), and enable --now's the
instances (idempotent, never restarts a running server). Also hardened the
section-1 *.env scan to skip the new playwright-*.env files (no T3_PORT -> grep
no-match would abort under set -e -o pipefail).
- Docs: chrome-service-snapshot runbook (new Provisioning section + system-unit
commands), multi-tenancy.md, and the 2026-06-07 plan Task 2.3.
Supersedes the hand-made per-user --user units (one-time idle-gated migration to
follow on the live host).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eb47eb1d10
commit
0a6ed4b2fe
11 changed files with 373 additions and 29 deletions
|
|
@ -11,8 +11,36 @@ external Claude Code sessions on the dev box. Architecture in
|
|||
| chrome-service Deployment | `chrome-service` ns | always-on | headed chromium, CDP :9222, persistent /profile/chromium-data |
|
||||
| snapshot-server sidecar | same pod | always-on | serves `/api/snapshot`, bearer-gated, port 8088 |
|
||||
| snapshot-harvester CronJob | `chrome-service` ns | `23 * * * *` | dumps `storage_state()` via CDP → `/profile/snapshots/storage-state.json` |
|
||||
| dev-box refresh timer | each dev box | hourly | curls `chrome.viktorbarzin.me/api/snapshot` → `~/.cache/playwright-shared-storage-state.json` |
|
||||
| dev-box `playwright-mcp.service` | each dev box | always-on | `@playwright/mcp --isolated --storage-state=…` per-MCP-connection contexts |
|
||||
| dev-box refresh timer | each dev box, per OS user | hourly (`*:28`) | `playwright-snapshot-refresh@<user>.timer` curls `chrome.viktorbarzin.me/api/snapshot` → `~/.cache/playwright-shared-storage-state.json` |
|
||||
| dev-box `playwright-mcp@<user>.service` | each dev box, per OS user | always-on | pinned `@playwright/mcp@<ver> --isolated --storage-state=…` on the user's `PLAYWRIGHT_PORT`; per-MCP-connection (per-session) contexts |
|
||||
|
||||
## Provisioning (reproducible from git)
|
||||
|
||||
The dev-box side is **per-OS-user** and fully reproducible — no hand-setup.
|
||||
Each user gets their own isolated `@playwright/mcp` server (multiple concurrent
|
||||
Claude sessions per user, isolated by `--isolated`), wired into their Claude in
|
||||
**every directory** via a user-scope `~/.claude.json` entry
|
||||
(`playwright → http://localhost:<PLAYWRIGHT_PORT>/mcp`).
|
||||
|
||||
- **System-level template units** (NOT `systemd --user`, so no linger needed):
|
||||
`playwright-mcp@.service` + `playwright-snapshot-refresh@.{service,timer}`,
|
||||
sourced from `infra/scripts/workstation/playwright/`, installed to
|
||||
`/etc/systemd/system/` by `setup-devvm.sh` (§9e). `User=%i`; per-user
|
||||
`PLAYWRIGHT_PORT` from `/etc/t3-serve/playwright-<user>.env`.
|
||||
- **Port allocation**: `roster_engine.py` (`PLAYWRIGHT_BASE_PORT=8931`, sticky)
|
||||
— emitted in the derive JSON, written per-user by `t3-provision-users.sh` (§5c).
|
||||
- **Snapshot token**: `setup-devvm.sh` (§8c) stages Vault
|
||||
`secret/chrome-service` `api_bearer_token` → root file
|
||||
`/etc/t3-serve/chrome-service-token`; the provisioner copies it (if-absent,
|
||||
0600) to each user's `~/.config/playwright/token` (the hourly root reconcile
|
||||
has no Vault token, hence the staging — mirrors the Claude OAuth token in §8a).
|
||||
- **MCP wiring + enablement**: `t3-provision-users.sh` `install_playwright()` runs
|
||||
`claude mcp add --scope user … playwright` AS the user (clobber-proof, if-absent)
|
||||
and `systemctl enable --now` the system instances. Idempotent; never restarts a
|
||||
running instance or rewrites an existing `~/.claude.json` entry.
|
||||
- **Pinned version**: bump `@playwright/mcp@<ver>` in
|
||||
`scripts/workstation/playwright/playwright-mcp@.service` (the `@latest` →
|
||||
silent-fleet-roll footgun is why; see the `T3_PIN` rationale in `setup-devvm.sh`).
|
||||
|
||||
## Day-to-day
|
||||
|
||||
|
|
@ -43,14 +71,14 @@ Expected: `wrote snapshot (… bytes) to /profile/snapshots/storage-state.json`.
|
|||
### Trigger dev-box refresh manually
|
||||
|
||||
```bash
|
||||
# On the dev box, as the user whose Claude Code sessions need the new state:
|
||||
systemctl --user start playwright-snapshot-refresh.service
|
||||
# On the dev box, refresh a specific user's snapshot (system template instance):
|
||||
sudo systemctl start playwright-snapshot-refresh@<user>.service
|
||||
|
||||
# Or directly:
|
||||
/usr/local/bin/playwright-snapshot-refresh
|
||||
# Or run the script directly AS that user:
|
||||
sudo -u <user> /usr/local/bin/playwright-snapshot-refresh
|
||||
|
||||
# Verify
|
||||
ls -la ~/.cache/playwright-shared-storage-state.json
|
||||
sudo ls -la /home/<user>/.cache/playwright-shared-storage-state.json
|
||||
```
|
||||
|
||||
### Inspect the current snapshot
|
||||
|
|
@ -108,12 +136,14 @@ The bearer token in `~/.config/playwright/token` doesn't match the
|
|||
server's. Almost always means the Vault secret was rotated and the
|
||||
local cache is stale.
|
||||
|
||||
**Fix**:
|
||||
**Fix** (re-stage centrally so a rebuild stays correct, then re-copy to the user):
|
||||
```bash
|
||||
vault login -method=oidc # if needed
|
||||
vault kv get -field=api_bearer_token secret/chrome-service > ~/.config/playwright/token
|
||||
chmod 600 ~/.config/playwright/token
|
||||
systemctl --user start playwright-snapshot-refresh.service
|
||||
sudo install -m 0600 <(vault kv get -field=api_bearer_token secret/chrome-service) \
|
||||
/etc/t3-serve/chrome-service-token
|
||||
sudo install -o <user> -g <user> -m 0600 \
|
||||
/etc/t3-serve/chrome-service-token /home/<user>/.config/playwright/token
|
||||
sudo systemctl start playwright-snapshot-refresh@<user>.service
|
||||
```
|
||||
|
||||
### Dev-box `playwright-snapshot-refresh` returns 404 with "snapshot not yet available"
|
||||
|
|
@ -129,9 +159,9 @@ new context with it. **Existing MCP sessions don't hot-reload** — they
|
|||
keep the cookies they were seeded with at session start. New sessions
|
||||
get the fresh snapshot.
|
||||
|
||||
**Fix**: restart the MCP server on the dev box to pick up the new file:
|
||||
**Fix**: restart the user's MCP server on the dev box to pick up the new file:
|
||||
```bash
|
||||
systemctl --user restart playwright-mcp.service
|
||||
sudo systemctl restart playwright-mcp@<user>.service
|
||||
```
|
||||
|
||||
### Snapshot file is suspiciously small or empty cookies array
|
||||
|
|
@ -158,13 +188,18 @@ vault kv put secret/chrome-service \
|
|||
|
||||
# Reloader auto-restarts chrome-service pod (snapshot-server picks up new token).
|
||||
|
||||
# On EVERY dev box that pulls the snapshot:
|
||||
vault kv get -field=api_bearer_token secret/chrome-service > ~/.config/playwright/token
|
||||
chmod 600 ~/.config/playwright/token
|
||||
# On EVERY dev box: re-stage the root file, then overwrite each user's copy
|
||||
# (the provisioner's per-user copy is if-absent, so a ROTATION must overwrite).
|
||||
sudo install -m 0600 <(vault kv get -field=api_bearer_token secret/chrome-service) \
|
||||
/etc/t3-serve/chrome-service-token
|
||||
for u in $(ls /etc/t3-serve/playwright-*.env 2>/dev/null | sed 's#.*/playwright-##;s#\.env##'); do
|
||||
sudo install -o "$u" -g "$u" -m 0600 \
|
||||
/etc/t3-serve/chrome-service-token /home/"$u"/.config/playwright/token
|
||||
done
|
||||
|
||||
# Verify the next refresh succeeds:
|
||||
systemctl --user start playwright-snapshot-refresh.service
|
||||
journalctl --user -u playwright-snapshot-refresh.service -n 20
|
||||
# Verify the next refresh succeeds for a user:
|
||||
sudo systemctl start playwright-snapshot-refresh@<user>.service
|
||||
sudo journalctl -u playwright-snapshot-refresh@<user>.service -n 20
|
||||
```
|
||||
|
||||
## Restore from a backup tarball
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue