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=mirrorouvre 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? }
| Module | Role |
|---|---|
src/features/ai/mirror/fetch-snapshot.ts | validateScanUrl + safeFetch, text/html, corps en flux sous 3 Mo. Liste dans outbound-fetch-revalidates.test.ts |
src/features/ai/mirror/sanitize-snapshot.ts | cheerio : scripts, on*, frames, formulaires, javascript: retires, URLs absolutisees, liens inertes |
src/features/ai/mirror/frame-document.ts | Le document servi, la CSP et les en-tetes, la page d'erreur au meme protocole |
src/features/ai/mirror/capture-script.ts | Le SEUL script du cadre : survol, selection, parent/enfant, serialisation des styles calcules |
src/features/ai/mirror/capture-schema.ts | La 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.ts | Les trois roles de marque, lus de StoreContext.modules.brandKit (forme wizard ou Figma) |
src/features/ai/mirror/install-section.ts | L'unique ecriture : decideThemeWrite sans allowLiveTheme, jamais d'ecrasement |
src/app/api/stores/[storeId]/mirror/frame/route.ts | Le snapshot encadre |
src/app/api/stores/[storeId]/mirror/sections/route.ts | Compile et installation |
src/features/ai/onlook/chrome/mirror/use-mirror-session.ts | L'etat d'une session (URL, selection, capture, compile, installation), partage par les trois pieces ci-dessous |
src/features/ai/onlook/chrome/mirror/mirror-panel.tsx | L'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.ts | L'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.tsx | Ce que montre le stage quand l'onglet est ouvert : le cadre isole et l'apercu compile |
src/features/ai/onlook/chrome/builder-toolbar.tsx | La 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.ts | La moitie parent du protocole |
Isolation : ce qui tient, et ou
- L'en-tete, pas le balisage.
/apiest hors du matcher du proxy, donc la reponse porte ses propres en-tetes.sandbox allow-scriptsen 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.tsepinglesandbox allow-scripts, unscript-srcreduit au nonce, etframe-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 accepteevent.source === iframe.contentWindowet 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) depuissrcdoc: 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)
- Aucun fichier du site source n'est exporte : images →
image_pickervides, SVG → glyphes integres, fondsurl()→ degrade de marque, polices → celles du theme. - 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. - Les couleurs sont reexprimees comme melanges des roles de marque (fond,
texte, accent), en reglages
colormodifiables. - 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.