ArchitectureMirror

Mirror

Pilier : ai-platform. Decisions : ADR 0044 (reconstruire sans copier), ADR 0045 (ou il vit). Emplacement : un onglet du Builder (?mode=builder, panneau lateral « Mirror », ou le bouton « Importer une section » de sa…

Pilier : ai-platform. Decisions : ADR 0044 (reconstruire sans copier), ADR 0045 (ou il vit). Emplacement : un onglet du Builder (?mode=builder, panneau lateral « Mirror », ou le bouton « Importer une section » de sa barre d'outils). Ce n'est plus un mode de la scene depuis l'ADR 0045 ; ?mode=mirror ouvre le Builder sur cet onglet.

Mirror transforme une section qu'un marchand pointe sur n'importe quelle page publique en section Shopify OS 2.0 reconstruite a la marque de sa boutique. Rien du site source n'est exporte : ni images, ni polices, ni feuilles de style, ni scripts, ni texte (sauf attestation de droits).

Le chemin d'une section

onglet Mirror du Builder (client)   serveur
─────────────                       ───────
URL collee ──► GET  /api/stores/[id]/mirror/frame?url=&token=
                    withSessionAuth (30/min) → authorizeStoreRoute(store.read)
                    → resolveEntitlement.unlocked → fetchMirrorSnapshot
                    → sanitizeSnapshot → buildMirrorFrame (nonce par reponse)
                    en-tetes mirrorFrameHeaders : CSP `sandbox allow-scripts`
iframe sandbox="allow-scripts"  ◄── mirror:ready / mirror:selected (postMessage)
capture desktop, puis a 390 px  ◄── mirror:captured
               ──► POST /api/stores/[id]/mirror/sections
                    withSessionAuth (CSRF, 20/min) → parseJsonBody (zod)
                    → parseMirrorCaptureSet → authorizeStoreRoute
                      (store.read pour compile, store.update pour install)
                    → resolveEntitlement → readStoreBrand
                    → compileMirrorSection → installMirrorSection (install)
apercu : iframe sandbox="" srcdoc   ◄── { section, install? }
ModuleRole
src/features/ai/mirror/fetch-snapshot.tsvalidateScanUrl + safeFetch, text/html, corps en flux sous 3 Mo. Liste dans outbound-fetch-revalidates.test.ts
src/features/ai/mirror/sanitize-snapshot.tscheerio : scripts, on*, frames, formulaires, javascript: retires, URLs absolutisees, liens inertes
src/features/ai/mirror/frame-document.tsLe document servi, la CSP et les en-tetes, la page d'erreur au meme protocole
src/features/ai/mirror/capture-script.tsLe SEUL script du cadre : survol, selection, parent/enfant, serialisation des styles calcules
src/features/ai/mirror/capture-schema.tsLa porte zod de la capture (taille, profondeur, cles de style en liste blanche)
src/features/ai/mirror/compile/*Capture → IR → Liquid + apercu. Pur, deterministe
src/features/ai/mirror/brand.ts, read-store-brand.tsLes trois roles de marque, lus de StoreContext.modules.brandKit (forme wizard ou Figma)
src/features/ai/mirror/install-section.tsL'unique ecriture : decideThemeWrite sans allowLiveTheme, jamais d'ecrasement
src/app/api/stores/[storeId]/mirror/frame/route.tsLe snapshot encadre
src/app/api/stores/[storeId]/mirror/sections/route.tsCompile et installation
src/features/ai/onlook/chrome/mirror/use-mirror-session.tsL'etat d'une session (URL, selection, capture, compile, installation), partage par les trois pieces ci-dessous
src/features/ai/onlook/chrome/mirror/mirror-panel.tsxL'onglet du panneau lateral, en trois etapes : Source (URL, selection, capture), Resultat (options, construction, rapport), Installer (cible, brouillon, installation)
src/features/ai/onlook/chrome/mirror/mirror-steps.tsL'etat de chaque etape (faite, en cours, a venir) et le nom traduit de chaque refus de la route
src/features/ai/onlook/chrome/mirror/mirror-stage.tsxCe que montre le stage quand l'onglet est ouvert : le cadre isole et l'apercu compile
src/features/ai/onlook/chrome/builder-toolbar.tsxLa barre flottante du Builder ; sa face Mirror porte selection, parent / enfant, appareil, Source / Resultat et Capturer
src/features/ai/onlook/chrome/mirror/use-mirror-frame.tsLa moitie parent du protocole

Isolation : ce qui tient, et ou

  • L'en-tete, pas le balisage. /api est hors du matcher du proxy, donc la reponse porte ses propres en-tetes. sandbox allow-scripts en CSP d'en-tete garde l'origine opaque meme si l'URL est ouverte en onglet ; frame-ancestors 'self' limite qui peut l'encadrer. Une CSP <meta> ignorerait les deux. route.test.ts epingle sandbox allow-scripts, un script-src reduit au nonce, et frame-ancestors 'self'.
  • Le message, par identite. Un cadre opaque poste avec event.origin === "null". Les ecouteurs de la Preview filtrent par origine et les ignoreraient ; celui de Mirror accepte event.source === iframe.contentWindow et le jeton du chargement, rien d'autre.
  • Aucun aller-retour depuis le cadre. connect-src 'none', form-action 'none', liens inertes. La capture remonte au parent, qui appelle l'API avec sa propre session.
  • L'apercu compile est rendu dans une iframe sandbox="" (aucun script) depuis srcdoc : le HTML est autonome (CSS inline, aucune URL distante), donc la CSP du tableau de bord dont il herite ne bloque rien.

Regles juridiques (non negociables)

  1. Aucun fichier du site source n'est exporte : images → image_picker vides, SVG → glyphes integres, fonds url() → degrade de marque, polices → celles du theme.
  2. Le texte est remplace par des placeholders de meme longueur. Le garder exige que le marchand coche l'attestation de droits ; sans elle, la route repond 422 rights_not_attested.
  3. Les couleurs sont reexprimees comme melanges des roles de marque (fond, texte, accent), en reglages color modifiables.
  4. L'installation ne vise que des themes non publies (409 sur le live, sans drapeau pour l'outrepasser) et n'ecrase jamais un fichier existant.

Dans le Builder

Mirror est un onglet du panneau lateral du Builder (Calques, Pages, Sections, Mirror), ouvert aussi par « Importer une section » dans sa barre flottante. Quand il est ouvert, le stage montre la page source et la barre d'outils prend sa face Mirror ; le canevas Onlook reste monte dessous, cache et inert. Le verrou Free est celui du Builder (FREE_LOCKED_STORE_TABS), et le chat recoit le cadrage builder, qui decrit aussi l'onglet Mirror (handler-prompt-fragments.ts). « Ask @Atlas » joint la selection par l'evenement boostecom:extension-element, comme le Builder et l'extension.

La cible d'installation est le theme que le Builder edite, plus un selecteur de brouillons : builder-library renvoie theme (id numerique, nom, live), resolu par pickBuilderTheme, la meme regle que builder-frame. Ce theme est celui que la Preview a choisi (previewThemeId, passe par ModeView au Builder puis en ?themeId= a chaque requete, ai-platform/3054) quand c'est un brouillon previsualisable ; sinon le meilleur brouillon (UNPUBLISHED, puis DEVELOPMENT, puis DEMO ; jamais ARCHIVED ni LOCKED, que Shopify ne previsualise pas ; MAIN en dernier recours, ai-platform/3069). Choisir « Alpha » dans la Preview fait donc editer « Alpha » au Builder et y installer Mirror. Un theme live n'est pas propose : l'etape Installer offre « Creer un brouillon du theme » (POST /api/stores/[storeId]/branches { create: true }, la route du Worktree), et la route d'installation le refuse toujours en 409. Une fois installee, la section est relue dans l'onglet Sections et ouverte sur le canevas (PagesManager.openThemeFile). Un refus de la route est affiche par son nom traduit (mirror-steps.ts), jamais par le message anglais du serveur.

Quand un element est selectionne sur le canevas, l'inspecteur de style s'ouvre a droite du stage (chrome/inspector/, ai-platform/3053) ; l'onglet Mirror ouvert, il reste ferme, le stage n'ayant pas d'element Onlook a inspecter. Son champ « Ask @Atlas » joint la selection par le meme evenement que ci-dessus. Carte complete du chrome : docs/audits/2026-09-26-builder-chrome.md.

Limites connues

  • Pas de rendu JavaScript : une page qui ne rend rien sans script donne un snapshot vide (emptyBody).
  • Les polices tierces sans Access-Control-Allow-Origin: * ne se chargent pas dans le snapshot (origine opaque) ; la section produite n'en depend pas.
  • Aucun outil @Atlas n'appelle encore Mirror : il agit sur ce que le marchand a selectionne et joint.