Audit — Système « Store Browser » (StoreBrowser) : 14 juin 2026
Audit complet du Store Browser : le super-node StoreBrowser (src/features/ai/preview/store-browser/, ~75 fichiers) qui est la surface principale du dashboard store (/[orgSlug]/[storeSlug]), son backend runtime…
Audit complet du Store Browser : le super-node
StoreBrowser(src/features/ai/preview/store-browser/, ~75 fichiers) qui est la surface principale du dashboard store (/[orgSlug]/[storeSlug]), son backend runtime (store-runtime), les routesapi/preview/browserbase/*
- proxy, les AI tools
tools/browser/*et les providerslib/browser/*.Méthode : 4 audits parallèles (panneaux détail · core + hooks · backend · tools IA), chaque constat rapporté avec sévérité / fichier:ligne / cause / correctif. Les constats critiques (P0/P1) ont été re-vérifiés à la main sur le code source (marqués
[vérifié]).
Périmètre & montage
/[orgSlug]/[storeSlug] → StoreOverviewClient → <StoreBrowser/> (surface principale)
/workflow → StoreBrowserNode (canvas multi-node)
shell app → <StoreBrowserProvider> (footer/left panels)
UI src/features/ai/preview/store-browser/ (store-browser.tsx 1623 l. + 60 composants/hooks/lib)
Backend src/features/store-runtime/session/service.ts (sessions Browserbase partagées par store)
API src/app/api/preview/browserbase/* + /api/preview/[storeId] + /proxy + /url-meta
Tools IA src/features/ai/tools/browser/* (connect/navigate/click/scroll/type/screenshot/get-dom)
Provider src/lib/browser/providers/* (browserbase/playwright + bright-data) — tier scan-engine
Synthèse
| Sévérité | Volume | Nature |
|---|---|---|
| P0 | 6 | Sécurité (SSRF agent + proxy, eval RCE, ownership session) + fuite coût (pas de reaper) + races cycle de vie |
| P1 | 14 | Features inachevées visibles opérateur (metrics hardcodés, terminal no-op, reprise de contrôle à sens unique, IntelligenceCard no-op) + bugs concrets |
| P2 | ~16 | Optimisation (polling redondant, CDP par requête, pas de dedupe fetch) + privacy ledger |
| P3 | ~12 | Code mort, commentaires obsolètes, micro-bugs cosmétiques |
État global : le Store Browser est une surface mature et largement
fonctionnelle (navigation CDP, viewport responsive, inspect mode, DevTools
Console/Network, sheets de config, versions, intelligence). Mais le câblage
final de plusieurs sous-systèmes annoncés « Phase 2/3/4 » n'a jamais été
terminé, et la couche sécurité du backend a des trous réels (le maillon
le plus exposé, le tool agent browserNavigate, est justement celui qui
saute le garde SSRF que le reste du code applique).
1. P0 — Sécurité (backend & tools)
1.1 [vérifié] Tool agent browserNavigate sans garde SSRF / same-origin
src/features/ai/tools/browser/navigate.ts:36-54
handle.page.goto(url) est appelé sur l'url fournie par le modèle avec
pour seule validation z.string().url().max(2048). Aucun validateScanUrl,
aucune restriction au domaine du store connecté : alors que les chemins
internes du service (service.ts:310 navigate() et :457
createAndCache()) appellent tous deux validateScanUrl. C'est le chemin le
plus atteignable (raisonnement non fiable du LLM → Chrome cloud avec cookies
Context persistants du marchand). Une injection de prompt dans une page
concurrente scrapée peut piloter Atlas vers http://169.254.169.254/… ou un
host interne et exfiltrer via browserGetDOM.
Correctif : validateScanUrl(url) dans execute avant goto + idéalement
contrainte d'host = domaine du store (ou allowlist).
1.2 [vérifié] Proxy storefront [storeId] : relais authentifié sans SSRF ni allowlist
src/app/api/preview/[storeId]/[[...path]]/route.ts:403-467
Le host upstream vient de Store.domain (texte libre opérateur) et est
fetché côté serveur (fetch(upstreamUrl, { redirect: "follow" })) sans
validateScanUrl ni allowlist, contrairement à proxy/route.ts qui
applique PREVIEW_ALLOWLIST + SSRF. Un Store.domain pointé sur un host
interne / 169.254.169.254 transforme la route en SSRF depuis l'egress de
l'app (pas un sandbox). redirect: "follow" aggrave (302 → interne). CSP du
document proxifié est *-permissive (défendable en isolation iframe, mais
combiné au point ci-dessus = exécution de HTML attaquant dans l'origine
proxy).
Correctif : validateScanUrl sur l'URL upstream résolue + allowlist domaine
Shopify/custom + redirect: "manual" re-validé à chaque hop.
1.3 Action eval arbitraire over CDP exposée à tout membre (y compris viewer)
src/app/api/preview/browserbase/active/action/route.ts:35-78 · service.ts:761-801
L'action eval exécute page.evaluate(action.expression) contre le Chrome
live (qui porte le Context persistant = session admin Shopify du marchand),
gardée uniquement par getStoreAccess (n'importe quel rôle). = RCE-in-store
pour tout membre. Les actions d'écriture (click/fill) ne vérifient ni
le rôle ni le verrou control (l'arbitrage Phase-3 n'est qu'un indice UI,
jamais appliqué côté serveur : cf. store-runtime/types.ts:75-76).
Correctif : restreindre eval (et les écritures) à admin/owner via
hasAccessPermission ; consulter meta.control et refuser les écritures
hors détenteur du verrou ; idéalement sortir eval de la surface HTTP.
1.4 [vérifié] Routes [id] Browserbase sans vérification d'ownership session
src/app/api/preview/browserbase/[id]/route.ts:31-37 ·
[id]/screenshot/route.ts:47-69 · [id]/logs/route.ts
Les routes ne gardent que withSessionAuth({ requireOrg: true }) puis
agissent sur l'id (session Browserbase) fourni par le client sans
vérifier que la session appartient à l'org de l'appelant. DELETE tue la
session (DoS) ; screenshot connecte en CDP et capture le viewport (fuite
pixel cross-tenant en théorie). Calibration : l'exploitation cross-org est
limitée par le fait que les ids de session sont des UUID Browserbase non
exposés entre orgs (le recordingUrl n'est visible que dans le
runtime-snapshot org-scopé) → risque réel mais sévérité P0→P1 ; le
bypass concret est intra-org (un viewer peut screenshot/tuer la session).
Correctif : lier la session à l'org (lookup byStore / table persistée) et
asserter meta.orgId === ctx.orgId avant d'agir.
1.5 [vérifié] Pas de reaper / TTL idle → sessions orphelines qui brûlent des minutes payantes
src/features/store-runtime/session/service.ts:400-411 (aucun cron)
Sessions créées en keepAlive: true + timeout 600 s. Seuls chemins de
release : DELETE client, ou teardown sur mismatch privacy. Aucun cron de
reaping (vérifié : rien dans vercel.json ni services/cron). Tab crash /
navigation / kill mobile → la session vit son timeout complet sans
comptabilité interne. Pire : le registre byStore est par instance lambda
(globalThis), donc un cold start orpheline toutes les entrées précédentes,
l'app ne peut plus jamais les release. Aggravé par l'absence de cap de
concurrence par org/user (le credit-gate a été retiré volontairement,
service.ts:155-163) : un user multi-stores peut spinner N sessions
payantes simultanées.
Correctif : cron qui liste les sessions Browserbase (userMetadata.source) et
release celles hors TTL ; persister les métadonnées session en Prisma
(release + accounting cross-instance) ; cap de concurrence par org.
1.6 Bright Data adapter : budget consommé en échec + credentials dans l'URL CDP
src/lib/browser/bright-data-adapter.ts:44-55,73
Budget réservé via rateLimit avant le fetch → un échec consomme un slot
(une cible qui flappe draine le budget $50/mo sans une seule page). Les
credentials sont interpolés dans l'URL WSS (wss://user:pass@…) ; si
connectOverCDP throw, Playwright inclut souvent l'URL dans le message
d'erreur → mot de passe dans les logs (catch logge err.message).
Correctif : réserver le budget après succès (ou refund en échec) ; passer les
creds via l'option auth du SDK, pas l'URL ; scrubber l'URL des logs.
(Note : tier scan-engine, voir §6, peut être non branché au Store Browser.)
2. P0/P1 — Cycle de vie session (races, leaks)
2.1 [vérifié] Stale closure : le toast d'échec « Live Browser » ne se déclenche jamais
store-browser.tsx:1058-1064
await live.start(absoluteUrl, { privacy: privacyMode })
if (live.status === "error" || live.error) { toast.error(...) } // ← live.* = snapshot pré-await
start renvoie Promise<void> ; après l'await, live.status/live.error
restent le snapshot capturé au render. Le toast d'échec ne part essentiellement
jamais (et peut faux-partir sur une erreur antérieure périmée).
Correctif : faire renvoyer { ok, error } par start et brancher sur la valeur
awaitée.
2.2 [vérifié] Races du cycle de vie (auto-start / viewport / privacy)
store-browser.tsx:317-327, 587-612, 784-802 + use-live-session.ts:121-123
Deux problèmes liés :
- Le garde de
startlit lestatuscapturé en closure (deps[status, storeId]).handleSetViewport/handleTogglePrivacyfontawait live.stop(); await live.start(...); maisstop()metstatus="idle"synchroniquement, ce qui re-déclenche l'effet auto-start (deps incl.live) →startSession()repart avec les anciens flags viewport/privacy, pendant que lestartexplicite de l'IIFE peut no-op (son closure voit encorestatus="live"). - L'effet auto-start latch
didAutoStartRef.current = storeIdavant d'appelerstartSession(); si lestartno-op sur unstatustransitoire, l'iframe reste surabout:blanksans retry (le retry n'arme que surerror). Correctif : sérialiser tout le cycle de vie dansuseLiveSession(méthoderestart({privacy,viewport})atomique sous untransitioningRef) ; latcherdidAutoStartRefseulement après succès ; le garde destartdoit lire unstatusRefsynchrone.
2.3 Inspect mode : inFlight jamais reset + uninstall inconditionnel (leak/race)
hooks/use-inspect-mode.ts:72-118
inFlight.current n'est jamais remis à false au cleanup → après un unmount
en plein tick, un re-enable rapide voit tous les ticks early-return et
l'inspecteur paraît mort. Le teardown fire INSPECT_UNINSTALL même si jamais
installé. Un install en vol juste avant cleanup peut atterrir après le
uninstall (inspecteur ré-injecté fantôme).
Correctif : reset inFlight.current=false au cleanup ; garder le uninstall
derrière un ref « was installed » ; AbortController sur runBrowserAction.
2.4 Effet idle-release re-bind le listener à chaque render → ne se déclenche jamais
store-browser.tsx:391-427
Deps = live (identité neuve à chaque render) → le listener visibilitychange
est détruit/re-ajouté à quasi chaque render, et hideTimer (closure-local) est
clear à chaque rebind. Or les polls 2s/3s garantissent un render < 5 min →
le timer d'idle-release de 5 min n'arrive jamais à terme. La fonctionnalité
« release la session après 5 min d'onglet caché » est donc inopérante (aggrave
§1.5).
Correctif : déps = live.status + control.state + refs stables ; timer en
useRef qui survit aux rebinds ; lire live.stop/startSession via refs.
2.5 Arbitrage du contrôle agent↔opérateur incomplet (sens unique + non persistant)
tools/browser/connect.ts:88-96 · hooks/use-browser-control.ts:110-126 · service.ts:60-92
[vérifié]releaseUser()est implémenté + renvoyé mais aucun composant ne l'appelle (grep = 0 call site). Après « Take over », il n'existe aucun chemin UI pour rendre la main à @Atlas → l'agent est verrouillé jusqu'au TTL 600 s. Hand-off câblé dans un seul sens.[vérifié]getBrowserPageappelleacquireControl(...)mais ignore le retour ;control_busyn'existe que dans un commentaire (service.ts:116). L'agent agit même quand l'opérateur détient le verrou (contredit la promesse Phase-3).- État
controlen mémoirebyStoreuniquement → un cold start serverless l'efface, et opérateur/agent peuvent vivre dans des lambdas différentes → arbitrage non cohérent multi-instance. L'auto-release du verrou agent stale est lazy (ne tourne que dansgetActive()). Correctif : bouton « Rendre la main / Release » câblé àreleaseUser()+ variante overlaystate === "user"; capturer le résultat d'acquireControlet renvoyercontrol_busy; persister l'état control en Redis/DB.
3. P1 — Pages détail / panneaux inachevés (visible opérateur)
3.1 [vérifié] Onglet Metrics : grille « Shopify analytics (14d) » codée en dur à —
components/store-inspector/metrics-tab.tsx:120-127
Revenue / Orders / AOV / Conv. rate sont des <Cell value='—'> littéraux :
aucun hook, aucun fetch, même pour un store OAuth connecté. La section est
habillée en paywall permanent (« Connect Shopify ») par-dessus des tirets
morts. C'est la surface « ça a l'air réel, c'est vide » la plus visible.
Correctif : hook useShopifyAnalytics(store.id) sur l'endpoint Admin analytics ;
paywall seulement si aucun connecteur Shopify actif, sinon vraies valeurs +
loading/empty.
3.2 Atlas Terminal : REPL statique, dispatch « not yet wired »
components/terminal-view.tsx:83-95,195-202
status imprime des placeholders (session: preview — not yet bridged to live runtime, tools: atlas router not yet wired, uptime: —) ; atlas <verb>
répond — preview, dispatch not yet wired. La fonctionnalité principale de
l'onglet est un no-op, mais l'opérateur peut taper des commandes valides en
apparence.
Correctif : câbler atlas au vrai routeur de verbes / bridge SSE, ou désactiver
l'input avec un empty-state « Coming soon » explicite.
3.3 [vérifié] Switchers Locale / Market : faux boutons (ouvrent juste Settings)
components/node-status-bar.tsx:215-246 (câblé store-browser.tsx:1561-1563)
Présentés comme bascules de viewport (« audit how the storefront renders for a
different country/language », icône Globe), mais onOpenLocales et
onOpenMarkets pointent tous deux sur () => setSettingsSheetOpen(true). Ils
ne re-rendent jamais l'iframe dans une autre locale/marché.
Correctif : implémenter le vrai preview (param ?locale=/marché + reload iframe)
ou relibeller en « Locale settings » pour matcher le comportement.
3.4 [vérifié] IntelligenceCard = no-op permanent (metadata.audit jamais écrit)
components/patterns/ai-elements/chat/intelligence-card.tsx:17-21 +
orchestrator/runtime/handler-message-metadata.ts:84-98
Le consommateur (assistant-message-extras.tsx:212) route bien meta.audit → <IntelligenceCard>, mais le producteur (seul assembleur de metadata par tour)
ne set jamais audit (grep = 0 assignation dans orchestrator). Travail
Phase-3 oublié. NB : un card kind audit existe en parallèle
(handler-card-tool.ts:98 → meta.cards/CardRouter) → le chemin
meta.audit est peut-être redondant/abandonné.
Correctif : soit peupler metadata.audit au runtime, soit supprimer
IntelligenceCard + la branche meta.audit et consolider sur le card kind.
3.5 Outils manquants pour l'agent navigateur
tools/browser/index.ts (6 tools : click/type/scroll/navigate/screenshot/getDOM)
Absents pour un agent qui pilote un navigateur : wait/waitForSelector,
select (<select>, fill ne marche pas), hover, extract/getText
(sinon dump outerHTML via getDOM = gâchis de contexte), fillForm, press
clavier, et l'historique goBack/goForward/reload (qui existent déjà sur
le service service.ts:512-533 mais ne sont pas exposés en tools).
Correctif : ajouter au minimum browserWaitFor, browserSelect, browserExtract
- exposer
goBack/forward/reloadetrunAction:extract.
3.6 Screenshots agent jamais persistés (Phase 6) → audit-replay impossible
tools/browser/screenshot.ts:5-8,57-95
Bytes round-trip uniquement ; le ledger note bytes mais aucune référence
d'artefact → la timeline Activity/replay ne peut pas reconstruire ce que
l'agent a vu. Les captures > 512 KB sont droppées (erreur typée) au lieu d'être
réduites.
Correctif : persister le PNG en Vercel Blob (stores/<id>/agent-screenshots/ <actionId>.png, helper déjà utilisé dans handler-tools-build.ts) + URL dans
le payload ; downscale au lieu de drop.
3.7 [vérifié] [id]/logs = stub permanent → Console/Network morts en session live
src/app/api/preview/browserbase/[id]/logs/route.ts
Renvoie toujours { logs: [], network: [] } (Phase 2 jamais livrée). Les
onglets DevTools Console/Network qui pollent toutes les ~3 s ne reçoivent rien
pour les sessions Browserbase live ; côté client use-console-logs n'a de plus
pas de dedupe (chaque tick appendCapped tout, id random) → duplication dès
que l'endpoint shippera.
Correctif : implémenter sur bb.sessions.logs(id) (avec ownership §1.4) ou pipe
WS → ring buffer Redis ; ajouter un curseur ?since= côté client.
3.8 [vérifié] providers/browserbase.ts : mauvais paramètre SDK (storeID vs projectId)
src/lib/browser/providers/browserbase.ts:45
bb.sessions.create({ storeID: this.storeID }), le SDK attend projectId
(tous les appels qui marchent dans service.ts / routes utilisent projectId).
Tout appel à ce provider échoue. (Tier scan-engine, probablement non utilisé
par le Store Browser : voir §6.)
Correctif : bb.sessions.create({ projectId: ... }) + renommer le champ.
3.9 Autres bugs P1 concrets
[vérifié]store-browser.tsx:1004-1006handleOpenExternal:window.open(url, "_blank")sansnoopener,noreferrer→ reverse tabnabbing + pas de feedback popup bloquée. Correctif :window.open(url, "_blank", "noopener,noreferrer").store-browser.tsx:1018-1030handleShareUrl:navigator.clipboardaccédé sans le gardetypeof navigator(présent danshandleCopyUrl) → peut throw.- « Open in Worktree » (
store-browser.tsx:725-737+scaffolds/elements.tsx:138-153) : l'eventboostecom:worktree:open-from-elementporte le snapshot mais le listener ne fait quesetMode("code")et jettedetail(commentaire admet le TODO). Le toast « Opening in Worktree… » promet plus que ce qui se passe. store-inspector.tsx:122-133:isGroundTruth = falsecodé en dur → un store Shopify OAuth affiche toujours « Estimated », jamais « Connected » (alors quemode-views.tsx:92-94calcule déjà le vrai flag depuisstore.connectors).
4. P2 — Performance / optimisation
4.1 Polling redondant sur /active (2s + 3s + 10s, non gardé visibilité)
use-browser-control.ts:87 (2s, pas de garde visibilité) ·
store-browser.tsx:475 (URL-sync 3s → /active/navigate) · :542 (heartbeat 10s)
~3 requêtes / 2-3 s en régime permanent sur la même session, chacune
ré-authentifiée (withSessionAuth + getStoreAccess = lookups DB par appel).
Le poll de contrôle tourne même onglet caché jusqu'au (non-fonctionnel, §2.4)
idle-release. Aucun dedupe inter-instances.
Correctif : un seul poller d'état session (1 fetch /active toutes N s) qui
fan-out control + active + URL aux abonnés, gardé visibilityState==="visible".
4.2 Connexion CDP par requête (backend + tools)
service.ts:285-334,512-546,808-840 · tools/browser/* (tous)
Chaque navigate/back/forward/reload/getCurrentUrl/getPageInsights/runAction et
chaque tool agent fait un connectOverCDP + browser.close() complet
(handshake WS de centaines de ms). Un tour Atlas (navigate→scroll→screenshot→
getDOM) ouvre 4 connexions CDP à la même session. L'URL-sync poll paie un
handshake toutes les ~3 s.
Correctif : cacher le Browser/Page connecté par session (et par tour agent) et
réutiliser, fermeture au release/expiry.
4.3 Fetches métadonnées sans cache/dedupe/AbortController
use-shopify-meta.ts · use-catalog-summary.ts · use-store-memory.ts · use-store-themes.ts
Per-instance useState+fetch, sans cache partagé ni SWR. Conséquences :
useShopifyMeta appelé 2× (profile-tab + market-tab) ; useStoreThemes
fetché 2× (direct + via use-theme-assets) ; pas d'AbortController → un
switch de store rapide peut poser une réponse périmée. Toutes mappent en plus
toute erreur (401/403/500) vers un état vide — sans signal opérateur
(une Custom App déconnectée vs un vrai 500 = actions très différentes).
Correctif : centraliser derrière StoreProvider (comme useStoreLedger/
useStoreReports) ou SWR keyé par storeId ; surfacer les erreurs réelles.
4.4 buildDevToolsFooterTabs reconstruit les 10 onglets à chaque poll
store-browser.tsx:839-859
Reconstruit les ReactNodes de tous les onglets (dont 7 scaffolds) à chaque
changement de logEntries/logNetwork : soit toutes les 3 s. Seuls
Console/Network dépendent des logs.
Correctif : mémoïser les scaffolds séparément ; pousser seulement les counts et
laisser le shell rendre les bodies à l'activation de l'onglet.
4.5 Aggregator runtime lourd sur un poll 6 s
store-runtime/context/aggregator.ts:172-194 · store-provider.tsx:88-99
<StoreProvider> poll runtime-snapshot toutes les 6 s ; chaque poll = 8
requêtes Neon parallèles dont agentAction.findMany({ take: 200 }) sans
select (blobs JSON payload/before/after), storeSnapshot take:50,
storeReport take:30 avec result complet.
Correctif : select ciblé sur agentAction ; réduire les take du chemin poll
(ledger complet en fetch à la demande) ; intervalle plus long ou SSE.
4.6 Divers P2
proxy/route.ts:88-101: cookies storefront upstream écrits sur l'origine dashboard enPath=/(envoyés sur chaque requête dashboard) ; seul le premierSet-Cookieest capté (multi-cookies Shopify perdus). Correctif : scoperpathà/api/preview/proxy+getSetCookie().url-meta/route.ts:107-140: SSRF validehttps://${hostname}/mais scrape l'hostname d'origine (port/path divergents), mismatch validation/usage.use-screenshot→store-browser.tsx:642-648:URL.revokeObjectURLsynchrone juste aprèsanchor.click()peut annuler le download (anchor jamais ajouté au DOM). Correctif :setTimeout(revoke, 0)+ append/remove.getPageInsights: dedupe seulement après la capture CDP (page-insights.ts: 333-357) → back/forward paie quand même le coût. Correctif :findRecentavant.type.ts:60: texte tapé écrit verbatim dans le ledger AgentAction (→ PlatformActivity). Seule garde = instruction prompt « never type secrets ». Risque PII/mot de passe journalisé en clair. Correctif : redaction/hash + preview masqué.ledger/route.ts,reports/latest,tasks,assets: capstakecodés (200/30/200/500) sans curseur exposé (assetsrenvoie un curseur que le GET ignore).reports/latestpeut starver un type si > 30 reports d'un autre type.
5. Inventaire « inachevé par design » (Phase 2/3/4)
Scaffolds/onglets fonctionnels en surface mais non câblés aux vraies données, attendus selon la roadmap mais visibles opérateur :
| Surface | État | Réf |
|---|---|---|
| DevTools Elements/Sources/Performance/Application/Logs/Activity/Tracking | scaffolds (branchés à useLatestReports/ledger/themeAssets — réels, pas mock, sauf détails ci-dessous) | dev-tools-drawer.tsx:304-345 |
| DevTools Console/Network (session live) | stub backend [id]/logs (§3.7) | — |
| Atlas Terminal | dispatch no-op (§3.2) | terminal-view.tsx |
| NodeStatusBar locale/market | placeholders (§3.3) | node-status-bar.tsx:36-42 |
| Inspector Metrics (Shopify analytics) | hardcodé — (§3.1) | metrics-tab.tsx |
| Inspector ground-truth badge | false codé en dur (§3.9) | store-inspector.tsx:122 |
| Détection de boutique protégée par mot de passe | regex sur l'URL uniquement, « Phase 3 wires a more reliable signal » | password-protected-banner.tsx:18 |
IntelligenceCard (chat) | no-op, metadata.audit jamais écrit (§3.4) | — |
performance.timing (déprécié) → métriques perf parfois null ; indexedDbDatabases: [] codé en dur | report APPLICATION/PERF partiel | service.ts:610-617,678 |
scaffolds/performance.tsx:60 : couleur de score via classe Tailwind
template-literal text-${...}-500 → non générée par le JIT v4 (interdit
ailleurs dans le repo, cf. viewport-frame.tsx:33-35) → couleur cassée.
Correctif : map statique de classes comme l'atome Metric.
6. Code mort / résidus
| Item | Réf |
|---|---|
SoonTab (placeholder onglets) — 0 call site (les 9 onglets sont câblés) | store-inspector/shared.tsx:22 |
CMP_BRAND_BY_NAME + commentaire « Consent tab » pendant (composant supprimé) | scaffolds/tracking.tsx:114-129 |
Exports extractMetric/PerfResultShape/TrackingResultShape dupliqués localement dans chaque scaffold | scaffolds/shared.tsx |
StoreAvatar (retiré mai 2026, seul NavDivider importé) + fallback img cassé | _pieces/store-avatar.tsx |
composeIframeSrc/stripProxyPrefix/statusWeight exportés non utilisés | _pieces/helpers.ts |
Bloc legacy *Local (drawer/storeInfo/versions) + void x setters | store-browser.tsx:682-715,816-817,867-869 |
{void store /* reserved */} rendu en JSX | scaffolds/elements.tsx:183 |
use-agent-ledger/use-latest-reports = shims DEPRECATED (error hardcodé null) | hooks |
| Commentaires obsolètes « 13 scaffold tabs » / liste d'onglets périmée | store-browser.tsx:1592-1597, dev-tools-drawer.tsx:24-26 |
providers/browserbase.ts + bright-data-adapter : tier scan-engine séparé du chemin Store Browser (logique Browserbase dupliquée, drift) | lib/browser/* |
7. Plan d'action priorisé
| Vague | Contenu | Risque |
|---|---|---|
| A — Sécurité (urgent) | §1.1 SSRF tool browserNavigate · §1.2 SSRF proxy [storeId] · §1.3 gating eval/écritures (rôle + control) · §1.4 ownership session [id] | Faible (ajout de gardes, pas de refonte) |
| B — Coût/fuites | §1.5 cron reaper + persistance session + cap concurrence · §1.6 budget/creds Bright Data · §4.1 consolider le polling | Moyen (cron + table Prisma) |
| C — Bugs cycle de vie | §2.1 stale closure toast · §2.2 sérialiser start/stop/restart · §2.3 leak inspect · §2.4 idle-release · §2.5 control handoff (releaseUser + control_busy + persistance) | Moyen |
| D — Pages détail | §3.1 metrics Shopify · §3.3 locale/market · §3.2 terminal · §3.7 logs live · §3.4 IntelligenceCard · §3.9 (noopener, ground-truth, share guard) · §5 perf tailwind | Faible→moyen |
| E — Perf | §4.2 réutilisation handle CDP · §4.3 SWR métadonnées · §4.4 mémo footer tabs · §4.5 aggregator select/take · §4.6 (cookies proxy, screenshot revoke, redaction type) | Faible |
| F — Complétude agent | §3.5 outils manquants · §3.6 persistance screenshots · service goBack/forward/reload exposés | Moyen |
| G — Nettoyage | §6 code mort + commentaires obsolètes | Nul |
Les 3 items les plus urgents : §1.1 (SSRF du tool agent, le LLM atteint un Chrome credentialé sans restriction d'host), §1.5 (pas de reaper → minutes payantes qui fuient + accounting faux), §2.5 (reprise de contrôle = piège à sens unique qui verrouille l'agent après un « Take over »).
8. Bilan d'exécution (14 juin 2026)
Corrections livrées sur la branche claude/wizardly-davinci-i1nsae
(pnpm typecheck clean, 447 tests verts). Par vague :
✅ Livré
| Réf | Correctif |
|---|---|
| §1.1 | browserNavigate : validateScanUrl + (legacy create route idem) |
| §1.2 | Proxy [storeId] : SSRF sur l'URL upstream + suivi manuel des redirects re-validé à chaque hop |
| §1.3 | Action route : actions d'écriture (eval/click/fill) gardées member+ (viewer en lecture seule exclu) |
| §1.4 | Routes [id] screenshot + DELETE : ownership via userMetadata.orgId |
| §1.6 | Bright Data : credentials scrubbés des logs d'erreur CDP |
| §2.1 | useLiveSession.start renvoie { ok, error } → toast d'échec « Live Browser » réellement déclenché |
| §2.2 | start lit un statusRef synchrone → plus de no-op au restart viewport/privacy |
| §2.3 | Inspect mode : reset inFlight au cleanup + UNINSTALL seulement si installé |
| §2.4 | Idle-release : bind une fois par store via refs latest-value → le timer 5 min se déclenche enfin |
| §2.5 | Control handoff bidirectionnel (overlay « Hand back to @Atlas » + releaseUser) + control_busy (navigate/click/type cèdent à l'opérateur) |
| §3.1 | Onglet Metrics : état honnête (plus de — codés sous un CTA « Connect Shopify » pour un store déjà connecté) |
| §3.3 | Locale/Market relibellés « settings » (matchent l'action réelle) |
| §3.9 | isGroundTruth dérivé des connecteurs Shopify actifs + noopener + garde navigator |
| §3.5 | Nouveaux outils d'agent browserWaitFor + browserExtract |
| §4.5 | Aggregator : select explicite sur le poll hot-path agentAction |
| §4.6 | Proxy proxy/route : cookies storefront scopés /api/preview/proxy (plus /) + capture multi-cookies (getSetCookie) |
| §4.6 | Tool type : texte tapé redacté dans le ledger (longueur + preview masqué) |
| §5 | performance.tsx : classe Tailwind statique (couleur de score réparée) ; probe : PerformanceNavigationTiming + enum IndexedDB async |
| §6 | Code mort retiré : SoonTab, CMP_BRAND_BY_NAME local + commentaire pendant, {void store} |
✅ Livré — 2ᵉ passe (infra)
| Réf | Correctif |
|---|---|
| §1.5 | Modèle Prisma BrowserSession (write-through, best-effort) + cron reaper browser-session-reaper (5 min : release des sessions expirées/orphelines idle > 3 min, comptabilité coût) + cap concurrence par org (8, éviction LRU à la création) + heartbeat lastActivityAt sur le GET /active |
| §2.5 | Control state persisté (acquire/release/stale-release) sur la row — visibilité opérateur + base cross-instance (le hot-path reste in-memory par design pour la latence) |
| §3.6 | Screenshots agent persistés en Vercel Blob (stores/<id>/agent-screenshots/) + artifactUrl au ledger ; les captures oversized sont stockées (plus droppées) |
| §3.7 | Route [id]/logs réelle : bb.sessions.logs.list → mapping des events CDP (Network/Console/Log) en entrées console/network, ownership via la row BrowserSession, curseur since ; client useConsoleLogs renvoie le curseur → plus de doublons |
| §4.2 | Pool de connexions CDP par session (refcount + idle-close 8 s + validation isConnected) dans getBrowserPage → un tour Atlas multi-tools réutilise un seul handshake |
| §4.3 | Hooks métadonnées routés via dedupedJson (in-flight dedupe → plus de requêtes shopify-meta/theme-assets dupliquées) + liveTheme/drafts mémoïsés |
⏭️ Laissé tel quel (décision assumée)
| Réf | Raison |
|---|---|
| §3.2 Atlas Terminal, §3.4 IntelligenceCard | Déjà honnêtes/inoffensifs (le terminal répond « not yet wired », la carte ne rend rien) — pas de surface trompeuse. |
§4.2 (cockpit runOnPage) | Le pool CDP est scopé aux tools agent (séquentiels). Le path cockpit (getCurrentUrl/navigate) garde le connect-par-appel — ses appels peuvent se chevaucher (poll URL 3 s), donc le pool y demande un test d'intégration live avant d'élargir. |