Audits · juin 2026Audit — Système « Store Browser » (StoreBrowser) : 14 juin 2026

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 routes api/preview/browserbase/*

  • proxy, les AI tools tools/browser/* et les providers lib/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éVolumeNature
P06Sécurité (SSRF agent + proxy, eval RCE, ownership session) + fuite coût (pas de reaper) + races cycle de vie
P114Features inachevées visibles opérateur (metrics hardcodés, terminal no-op, reprise de contrôle à sens unique, IntelligenceCard no-op) + bugs concrets
P2~16Optimisation (polling redondant, CDP par requête, pas de dedupe fetch) + privacy ledger
P3~12Code 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 start lit le status capturé en closure (deps [status, storeId]). handleSetViewport/handleTogglePrivacy font await live.stop(); await live.start(...) ; mais stop() met status="idle" synchroniquement, ce qui re-déclenche l'effet auto-start (deps incl. live) → startSession() repart avec les anciens flags viewport/privacy, pendant que le start explicite de l'IIFE peut no-op (son closure voit encore status="live").
  • L'effet auto-start latch didAutoStartRef.current = storeId avant d'appeler startSession() ; si le start no-op sur un status transitoire, l'iframe reste sur about:blank sans retry (le retry n'arme que sur error). Correctif : sérialiser tout le cycle de vie dans useLiveSession (méthode restart({privacy,viewport}) atomique sous un transitioningRef) ; latcher didAutoStartRef seulement après succès ; le garde de start doit lire un statusRef synchrone.

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é] getBrowserPage appelle acquireControl(...) mais ignore le retour ; control_busy n'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 control en mémoire byStore uniquement → 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 dans getActive()). Correctif : bouton « Rendre la main / Release » câblé à releaseUser() + variante overlay state === "user" ; capturer le résultat d'acquireControl et renvoyer control_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/reload et runAction: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-1006 handleOpenExternal : window.open(url, "_blank") sans noopener,noreferrer → reverse tabnabbing + pas de feedback popup bloquée. Correctif : window.open(url, "_blank", "noopener,noreferrer").
  • store-browser.tsx:1018-1030 handleShareUrl : navigator.clipboard accédé sans le garde typeof navigator (présent dans handleCopyUrl) → peut throw.
  • « Open in Worktree » (store-browser.tsx:725-737 + scaffolds/elements.tsx:138-153) : l'event boostecom:worktree:open-from-element porte le snapshot mais le listener ne fait que setMode("code") et jette detail (commentaire admet le TODO). Le toast « Opening in Worktree… » promet plus que ce qui se passe.
  • store-inspector.tsx:122-133 : isGroundTruth = false codé en dur → un store Shopify OAuth affiche toujours « Estimated », jamais « Connected » (alors que mode-views.tsx:92-94 calcule déjà le vrai flag depuis store.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 en Path=/ (envoyés sur chaque requête dashboard) ; seul le premier Set-Cookie est capté (multi-cookies Shopify perdus). Correctif : scoper path à /api/preview/proxy + getSetCookie().
  • url-meta/route.ts:107-140 : SSRF valide https://${hostname}/ mais scrape l'hostname d'origine (port/path divergents), mismatch validation/usage.
  • use-screenshot → store-browser.tsx:642-648 : URL.revokeObjectURL synchrone juste après anchor.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 : findRecent avant.
  • 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 : caps take codés (200/30/200/500) sans curseur exposé (assets renvoie un curseur que le GET ignore). reports/latest peut 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ÉtatRéf
DevTools Elements/Sources/Performance/Application/Logs/Activity/Trackingscaffolds (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 Terminaldispatch no-op (§3.2)terminal-view.tsx
NodeStatusBar locale/marketplaceholders (§3.3)node-status-bar.tsx:36-42
Inspector Metrics (Shopify analytics)hardcodé — (§3.1)metrics-tab.tsx
Inspector ground-truth badgefalse codé en dur (§3.9)store-inspector.tsx:122
Détection de boutique protégée par mot de passeregex 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 durreport APPLICATION/PERF partielservice.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

ItemRé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 scaffoldscaffolds/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 settersstore-browser.tsx:682-715,816-817,867-869
{void store /* reserved */} rendu en JSXscaffolds/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éestore-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é

VagueContenuRisque
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 pollingMoyen (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 tailwindFaible→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ésMoyen
G — Nettoyage§6 code mort + commentaires obsolètesNul

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éfCorrectif
§1.1browserNavigate : validateScanUrl + (legacy create route idem)
§1.2Proxy [storeId] : SSRF sur l'URL upstream + suivi manuel des redirects re-validé à chaque hop
§1.3Action route : actions d'écriture (eval/click/fill) gardées member+ (viewer en lecture seule exclu)
§1.4Routes [id] screenshot + DELETE : ownership via userMetadata.orgId
§1.6Bright Data : credentials scrubbés des logs d'erreur CDP
§2.1useLiveSession.start renvoie { ok, error } → toast d'échec « Live Browser » réellement déclenché
§2.2start lit un statusRef synchrone → plus de no-op au restart viewport/privacy
§2.3Inspect mode : reset inFlight au cleanup + UNINSTALL seulement si installé
§2.4Idle-release : bind une fois par store via refs latest-value → le timer 5 min se déclenche enfin
§2.5Control handoff bidirectionnel (overlay « Hand back to @Atlas » + releaseUser) + control_busy (navigate/click/type cèdent à l'opérateur)
§3.1Onglet Metrics : état honnête (plus de — codés sous un CTA « Connect Shopify » pour un store déjà connecté)
§3.3Locale/Market relibellés « settings » (matchent l'action réelle)
§3.9isGroundTruth dérivé des connecteurs Shopify actifs + noopener + garde navigator
§3.5Nouveaux outils d'agent browserWaitFor + browserExtract
§4.5Aggregator : select explicite sur le poll hot-path agentAction
§4.6Proxy proxy/route : cookies storefront scopés /api/preview/proxy (plus /) + capture multi-cookies (getSetCookie)
§4.6Tool type : texte tapé redacté dans le ledger (longueur + preview masqué)
§5performance.tsx : classe Tailwind statique (couleur de score réparée) ; probe : PerformanceNavigationTiming + enum IndexedDB async
§6Code mort retiré : SoonTab, CMP_BRAND_BY_NAME local + commentaire pendant, {void store}

✅ Livré — 2ᵉ passe (infra)

RéfCorrectif
§1.5Modè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.5Control 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.6Screenshots agent persistés en Vercel Blob (stores/<id>/agent-screenshots/) + artifactUrl au ledger ; les captures oversized sont stockées (plus droppées)
§3.7Route [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.2Pool 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.3Hooks 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éfRaison
§3.2 Atlas Terminal, §3.4 IntelligenceCardDé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.