CodeGym /Kurse /ChatGPT Apps /Arbeiten in der Sandbox: Einschränkungen, Feinheiten und ...

Arbeiten in der Sandbox: Einschränkungen, Feinheiten und window.openai

ChatGPT Apps
Level 3 , Lektion 0
Verfügbar

1. Was ist die Sandbox und warum steckt Ihr Widget im Käfig

Wenn ChatGPT Ihr Widget anzeigt, rendert es dieses nicht wie eine gewöhnliche <iframe src="https://ihre-seite">-Seite. Das Widget läuft in einer verwalteten „Sandbox“ — einem isolierten iframe mit eigener Origin und strikten Sicherheitseinstellungen.

Technisch sieht das etwa so aus:

flowchart TD
    User["Benutzer in ChatGPT"]
    Chat["ChatGPT UI + Modell"]
    Iframe["Ihr Widget
sandboxed iframe"] MCP["Ihr MCP / Backend"] User --> Chat Chat -->|Tool-Aufruf| MCP MCP -->|structuredContent + _meta| Chat Chat -->|window.openai.*| Iframe Iframe -->|callTool / follow-up| Chat Chat --> MCP

Ihr Code wird nur innerhalb dieses iframes ausgeführt, und der Zugriff auf den Rest der Welt erfolgt über ein eng kontrolliertes API, das der Host (ChatGPT) bereitstellt. Das Widget darf nicht:

  • ChatGPT selbst kaputtmachen (DOM, Styles, Performance);
  • die Privatsphäre der Nutzer verletzen;
  • unkontrolliert ins Netz telefonieren.

Daraus ergeben sich die wichtigsten Einschränkungen der Sandbox.

Isolation von DOM und Origin

Das Widget lebt auf einer speziellen Sandbox-Domain (zum Beispiel https://sandbox-apps.oaiusercontent.com) mit dem Attribut sandbox am iframe. Das bedeutet:

  • Sie können nicht in window.parent oder das document von ChatGPT greifen — Sie erhalten einen SecurityError;
  • Cross-Domain-Mechanismen wie postMessage werden vom Host kontrolliert;
  • jeder Versuch, die „ChatGPT-Oberfläche per eigenem CSS zu reparieren“, ist zum Scheitern verurteilt.

Netzwerk- und CSP-Einschränkungen

Browser und CSP-Policy des Hosts begrenzen den Netzwerkzugriff für Ihr Widget:

  • die Methode fetch hat Zugriff nur auf Domains aus einer Whitelist, die ein Review durchlaufen müssen;
  • welche Domains aus dem Widget erreichbar sind, deklarieren Sie explizit über openai/widgetCSP in den MCP-Antworten; sonst werden Anfragen einfach blockiert;
  • der empfohlene Weg für alles Ernsthafte ist: gar nicht aus dem Widget ins Netz gehen, sondern über MCP-Tools und callTool den Backend ansprechen (mehr dazu in Modul 4).

Praktisch: Denken Sie über das Widget als dünne UI-Schicht. Es spricht mit ChatGPT und Ihrem Server über streng definierte Kanäle, nicht wie ein normales SPA, das frei im Internet lebt.

Speicher und Ressourcen

Lokale Speicher (localStorage, sessionStorage) stehen zur Verfügung, Cookies hingegen nicht. Berücksichtigen Sie das beim Entwickeln. Speicher und CPU sind begrenzt: Wenn Sie im Widget alle Primzahlen bis zu einer Milliarde berechnen, hat der Host jedes Recht, Ihren iframe einfach zu beenden.

Wichtige Konsequenz: keine schweren Berechnungen und langlebigen „Caches“ im Widget. Komplexe Logik gehört auf die Serverseite, nicht in den React-Component.

2. window.openai: Brücke zwischen Widget und ChatGPT

Damit das Widget überhaupt etwas erfährt (Tool-Ergebnisse, Anzeigemodus, Locale, Zustand), injiziert ChatGPT bei der Initialisierung in das iframe-Fenster ein globales Objekt — window.openai.

Das ist nicht Ihr Code und kein npm-Paket, sondern ein Host-Objekt, das die KI-Plattform bereitstellt. Unter der Haube basiert es auf Events und Nachrichten zwischen Host und iframe, aber darum müssen Sie sich kaum kümmern. Wichtig sind ein paar Punkte.

Wer und wann erstellt window.openai

window.openai existiert nur:

  • innerhalb des iframes, den ChatGPT für Ihr Widget erstellt hat;
  • wenn das HTML-Template mit dem richtigen mimeType (text/html+skybridge) ausgeliefert und alle Prüfungen bestanden hat.

Diesen Typ haben Sie im Modul zur HelloWorld App schon gesehen — genau den liefert die Widget-Seite anstelle von text/html.

Wenn Sie die Widget-Seite direkt im Browser öffnen, gilt:

console.log(window.openai); // undefined

und das ist normal. Daher sollte Ihr Widget-Code immer prüfen, ob das Objekt existiert, wenn Sie mit einem „Standalone“-Modus für lokale Entwicklung oder Storybook rechnen.

Primitives Beispiel (nicht final, nur zur Illustration):

if (typeof window !== "undefined" && (window as any).openai) {
  console.log("We are inside ChatGPT sandbox!");
}

Asynchronität der Initialisierung

Unter der Haube aktualisiert ChatGPT window.openai, sobald neue Daten eintreffen (neues toolOutput, Wechsel des displayMode usw.), und nutzt dafür das interne Event openai:set_globals.

Die „Werte“ darin sind also nicht statisch: Das KI-Modell kann ein MCP-Tool aufrufen, der Backend liefert neue structuredContent, und window.openai.toolOutput ändert sich direkt unter Ihrem React-Component.

Daraus folgen zwei Empfehlungen:

  1. Keine blinden Snapshots der Form const toolOutput = window.openai.toolOutput einmal zu Beginn nehmen und für immer gültig halten. Ein und dasselbe Widget kann von ChatGPT wiederverwendet werden.
  2. Den Hook-Layer verwenden (gleich mehr), der Änderungen abonnieren kann.

3. Anatomie von window.openai: Daten, API und Kontext

Die offizielle Dokumentation bietet eine recht kompakte Tabelle mit Feldern und Methoden von window.openai. Wir fassen sie in einer „menschlicheren“ Variante zusammen.

Wesentliche Felder und Methoden

window.openai = {
  // State & data
  toolInput,               // JSON: Parameter, die die KI an Ihr MCP-Tool übergeben hat
  toolOutput,              // JSON: Parameter, die Ihr MCP-Tool an die KI zurückgegeben hat
  toolResponseMetadata,    // Antwort des MCP-Tools: Teil von _meta: {...}
  widgetState,             // Gespeicherten Widget-Zustand lesen
  setWidgetState,          // Den Zustand Ihres Widgets hier speichern

  // Runtime APIs
  callTool,                // MCP-Tool aufrufen
  sendFollowUpMessage,     // Unauffällig eine Nachricht an die KI im Chat senden: Sie beginnt zu antworten.
  requestDisplayMode,      // Widget in einen anderen Modus schalten: fullscreen, pip, inline
  requestModal,            // Widget in ein modales Fenster verwandeln.
  requestClose,            // Schließt das Widget. Schließt ein modales Fenster – es wird wieder zum Widget.
  requestCheckout,         // Öffnet ein Bezahl-Modal. Der Server muss ACP implementieren
  notifyIntrinsicHeight,   // Änderung der Widget-Höhe melden
  openExternal,            // Link in einem neuen Fenster öffnen.

  // Context
  theme,                   // Dunkles oder helles Theme
  displayMode,             // Aktueller Anzeigemodus des Widgets, kann von requestDisplayMode abweichen
  maxHeight,               // Maximal zulässige Höhe des Widgets
  safeArea,                // „Sicherer Zeichenbereich“ – relevant für Telefone mit „Notch“
  view,
  userAgent,               // userAgent des Browsers
  locale                   // locale des Browsers
}

Dasselbe als Tabelle:

Kategorie Eigenschaft / Methode Wozu
State & data
toolInput
Argumente, mit denen das Tool aufgerufen wurde. Read-only.
State & data
toolOutput
Ihr structuredContent aus der MCP-Antwort. Was Widget und Modell sehen.
State & data
toolResponseMetadata
_meta aus der Antwort. Nur für das Widget sichtbar, das Modell liest es nicht.
State & data
widgetState
Snapshot des UI-Zustands, den ChatGPT zwischen Renders des Widgets speichert.
State & data
setWidgetState(state)
Neuen Snapshot von widgetState synchron speichern.
Function
callTool(name, args)
MCP-Tool aus dem Widget aufrufen.
Function
sendFollowUpMessage({prompt})
ChatGPT bitten, eine Nachricht im Chat im Namen des Widgets zu senden. Es beginnt zu antworten.
Function
requestDisplayMode(...)
Beim Host inline / fullscreen / pip erbitten.
Function
requestModal({title})
Öffnen eines modalen Fensters anfordern.
Function
notifyIntrinsicHeight()
Mitteilen, dass sich die Höhe des Inhalts geändert hat.
Function
requestCheckout(...)
Öffnet den Zahlungsdialog gemäß ACP-Protokoll.
Function
openExternal({href})
Externen Link im Browser des Nutzers öffnen.
Context
theme, displayMode, maxHeight, safeArea, view, userAgent, locale
Signale der Umgebung: Theme, Modus, verfügbare Höhe, Locale usw.

Sie müssen sich nicht alles aus dieser Tabelle sofort merken — betrachten Sie sie als „Landkarte“. Jetzt nähern wir uns dem Thema weniger als „Nachschlagewerk“, sondern mehr pragmatisch.

toolInput und toolOutput: Woher kommen die Daten

Wenn das Modell Ihr Tool aufruft, formt es JSON-Argumente. Diese Argumente:

  • kommen am MCP-Server als input im Handler an;
  • landen gleichzeitig im window.openai.toolInput im Widget.

Nach der Ausführung liefert der Server zurück:

  • structuredContent — strukturierte Daten für die UI;
  • _meta — private Daten nur fürs Widget;
  • content — Text fürs Modell selbst, damit es dem Nutzer „erzählen“ kann, was passiert ist.

structuredContent wird zu window.openai.toolOutput, und _meta wird zu window.openai.toolResponseMetadata.

Mini-Beispiel (Vanilla JS, ohne React):

const root = document.getElementById("root");

// Den Nullish-Operator kann man bedenkenlos verwenden
const gifts = window.openai.toolOutput?.gifts ?? [];

root.textContent = `Gefundene Geschenke: ${gifts.length}`;

widgetState und setWidgetState: Gedächtnis des Widgets

widgetState ist das, was die Plattform über Ihre UI zwischen Renders und sogar zwischen einzelnen Dialogzügen bereit ist zu merken.

Beispiele sinnvoller Inhalte für widgetState:

  • das ausgewählte Geschenk;
  • aktuelle Sortierung (nach Preis / nach Popularität);
  • Seitennummer in einer Liste.

Nicht geeignet:

  • rohe Antwort eines externen APIs;
  • Bild in base64;
  • geheime Tokens.

Wichtig sind zwei Dinge:

  1. widgetState wird zusammen mit dem Kontext gespeichert und ans Modell übergeben; daher gehören keine sensiblen Daten hinein.
  2. Das Volumen ist begrenzt (etwa 4 000 Tokens), also kein Mini-Datenbankersatz.

Einfachstes Beispiel (direkt, ohne Hooks, in Vanilla JS):

const current = window.openai.widgetState ?? { selectedGiftId: null };

function selectGift(id) {
  window.openai.setWidgetState({ ...current, selectedGiftId: id });
}

Im echten Code kapseln wir das in React-Hooks.

Runtime-API: callTool, sendFollowUpMessage und Co.

Diese Methoden erlauben dem Widget nicht nur „zu zeichnen“, sondern auch mit Dialog und Server zu interagieren.

Einige typische Szenarien:

  • callTool("search_gifts", { budget: 50 }) — der Nutzer klickt auf „Budget ändern“, Sie rufen den Server und aktualisieren die UI;
  • sendFollowUpMessage({ prompt: "Zeig noch teurere Ideen" }) — statt den Nutzer manuell Text tippen zu lassen, fügen Sie einen Follow-up-Button hinzu, der eine neue Nachricht im Chat erstellt;
  • requestDisplayMode({ mode: "fullscreen" }) — wenn der Inline-Modus zu eng wird, kann das Widget höflich um Vollbild bitten;
  • openExternal({ href: "https://myshop.com/checkout?giftId=123" }) — den Nutzer über einen geprüften Kanal auf eine externe Seite schicken (Checkout, Profil usw.).

All das läuft „über die Leitung“ via ChatGPT — nicht direkt ins Internet.

Umgebungskontext: Theme, Modus, Höhe, Locale

Felder wie theme, displayMode, maxHeight, locale vermitteln, in welcher Umgebung das Widget lebt.

Zum Beispiel:

const theme = window.openai.theme;          // "light" oder "dark"
const mode = window.openai.displayMode;     // "inline" | "fullscreen" | "pip"
const maxH = window.openai.maxHeight;       // verfügbare Höhe
const locale = window.openai.locale;        // "en-US", "de-DE", ...

Mit diesen Signalen können Sie:

  • Farben und Abstände ans Theme anpassen;
  • Layout je nach Modus ändern (inline vs fullscreen);
  • Beschriftungen im UI an die Sprache der Nutzer anpassen (dazu gibt es noch ein eigenes Modul).

Die Plattform liefert Signale zu Platz, Theme und Locale. Sinnvoll ist die Nutzung über useOpenAIGlobal, useDisplayMode, useMaxHeight und andere Hooks, damit sich das Widget in ChatGPT „heimisch“ anfühlt.

4. Hooks über window.openai: Den Global nicht per Hand anfassen

Der direkte Zugriff auf window.openai ist für einen Prototyp bequem, verwandelt den Code aber schnell in Chaos: Event-Subscriptions, undefined-Checks, wiederkehrende Wrapper. Deshalb gibt es im Next.js-Template für das Apps SDK einen fertigen Satz React-Hooks, die Details verstecken und alles reaktiv machen.

Ein typischer Hook-Index sieht so aus:

// app/hooks/openai/index.ts
export { useCallTool } from "./use-call-tool";
export { useSendMessage } from "./use-send-message";
export { useOpenExternal } from "./use-open-external";
export { useRequestDisplayMode, useRequestModal, useRequestClose } from "./use-request-display-mode";
export { useRequestCheckout } from "./use-request-checkout";

// State hooks
export { useDisplayMode } from "./use-display-mode";
export { useWidgetProps } from "./use-widget-props";
export { useWidgetState } from "./use-widget-state";
export { useOpenAIGlobal } from "./use-openai-global";

export { useMaxHeight } from "./use-max-height";
export { useIsChatGptApp } from "./use-is-chatgpt-app";

Bezeichnungen und genaue Pfade können in Ihrem Template leicht abweichen, aber die Idee ist überall dieselbe: Statt window.openai.* nutzen Sie Hooks. Schauen wir uns die wichtigsten an.

useWidgetProps: Eingabe und Ausgabe des Tools

useWidgetProps liefert in der Regel ein Objekt mit den Daten, die das Widget braucht: toolInput, toolOutput, toolResponseMetadata und manchmal zusätzliche Flags wie isLoading.

Beispiel:

import { useWidgetProps } from "../hooks/openai";

type Gift = { id: string; title: string; price: number };

export function GiftList() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  if (!gifts.length) {
    return <div>Noch keine Geschenkideen.</div>;
  }

  return (
    <ul>
      {gifts.map((g) => (
        <li key={g.id}>{g.title} — ${g.price}</li>
      ))}
    </ul>
  );
}

Kein window.openai im Komponenten­code — und das ist gut so.

useWidgetState: „Reaktive Hülle“ über widgetState

useWidgetState erlaubt, mit widgetState wie mit normalem React-State zu arbeiten: Sie bekommen [state, setState], und der Hook synchronisiert ihn unter der Haube mit window.openai.widgetState und setWidgetState.

Beispiel:

import { useWidgetState } from "../hooks/openai";

type UiState = { selectedGiftId: string | null };

export function SelectedGiftIndicator() {
  const [uiState, setUiState] = useWidgetState<UiState>(() => ({
    selectedGiftId: null,
  }));

  if (!uiState?.selectedGiftId) {
    return <div>Noch kein Geschenk ausgewählt.</div>;
  }

  return (
    <div>
      Sie haben ein Geschenk mit id={uiState.selectedGiftId} ausgewählt
      <button onClick={() => setUiState({ selectedGiftId: null })}>
        Zurücksetzen
      </button>
    </div>
  );
}

Nach dem Klick aktualisiert setUiState nicht nur den React-State, sondern speichert den neuen Zustand auch auf der ChatGPT-Seite.

useOpenAIGlobal: Zugriff auf ein beliebiges Feld von window.openai

Wenn Sie auf ein einzelnes globales Feld (z. B. Theme oder Modus) zugreifen möchten, gibt es den universellen Hook useOpenAIGlobal(key). Er abonniert das Event openai:set_globals und liefert stets den aktuellen Wert.

Beispiel:

import { useOpenAIGlobal } from "../hooks/openai";

export function ThemeAwareBlock() {
  const theme = useOpenAIGlobal<"light" | "dark">("theme");

  const background = theme === "dark" ? "#222" : "#fff";
  const color = theme === "dark" ? "#fff" : "#000";

  return <div style={{ background, color }}>Ich respektiere das ChatGPT-Theme</div>;
}

useCallTool, useSendMessage, useOpenExternal und weitere

  • useCallTool(name) — liefert eine Funktion, die das MCP-Tool mit dem angegebenen Namen aufruft. Wrapper um callTool.
  • useSendMessage() — kapselt sendFollowUpMessage, damit das Widget Nachrichten initiieren kann.
  • useOpenExternal() — praktischer Helfer um openExternal({ href }).
  • useRequestDisplayMode() und useRequestModal() — Wrapper für Moduswechsel- bzw. Modal-Requests.

Grundbeispiel des Mini-Widgets GiftGenius, das fast alles auf einmal nutzt:

import {
  useWidgetProps,
  useWidgetState,
  useCallTool,
  useSendMessage,
  useOpenExternal,
} from "../hooks/openai";

type Gift = { id: string; title: string; url: string; price: number };

export function GiftWidget() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const gifts = toolOutput?.gifts ?? [];

  const [ui, setUi] = useWidgetState<{ selectedId: string | null }>(() => ({
    selectedId: null,
  }));

  const callSearch = useCallTool("search_gifts");
  const sendMessage = useSendMessage();
  const openExternal = useOpenExternal();

  if (!gifts.length) {
    return <div>Noch keine Ideen. Bitten Sie GPT, die Ergebnisse zu aktualisieren.</div>;
  }

  return (
    <div>
      {gifts.map((g) => (
        <button
          key={g.id}
          style={{
            display: "block",
            fontWeight: ui?.selectedId === g.id ? "bold" : "normal",
          }}
          onClick={() => setUi({ selectedId: g.id })}
        >
          {g.title} — ${g.price}
        </button>
      ))}

      <div style={{ marginTop: 12 }}>
        <button
          onClick={() =>
            sendMessage({ prompt: "Zeig Geschenke, die teurer sind als die aktuellen." })
          }
        >
          Weitere Ideen anfragen
        </button>

        <button
          onClick={async () => {
            await callSearch({ budget: 200 });
          }}
        >
          Mit Budget $200 aktualisieren
        </button>

        {ui?.selectedId && (
          <button
            onClick={() =>
              openExternal({
                href: `https://giftgenius.example.com/checkout?id=${ui.selectedId}`,
              })
            }
          >
            Zum Kauf gehen
          </button>
        )}
      </div>
    </div>
  );
}

Diese Seite ist noch roh (wir verbessern in den nächsten Modulen UX, Fehlermanagement usw.), aber sie illustriert bereits den Ansatz: keine direkten Zugriffe auf window.openai, nur Hooks.

5. Praxis: Sandbox und window.openai erkunden

Um zu spüren, was „Widget nicht wie eine normale Website“ bedeutet, helfen ein paar Übungen.

Übung: „Teste die Umgebung“

Nehmen Sie Ihre aktuelle app/page.tsx im Widget und fügen Sie beim ersten Render einen einfachen Effekt hinzu:

import { useEffect } from "react";
import { useIsChatGptApp } from "../hooks/openai";

export default function Root() {
  const isChatGpt = useIsChatGptApp();

  useEffect(() => {
    if (typeof window !== "undefined") {
      console.log("window.origin =", window.origin);
      console.log("window.openai =", (window as any).openai);
    }
  }, []);

  return (
    <main>
      <h1>GiftGenius widget</h1>
      <p>Innerhalb von ChatGPT gestartet: {String(isChatGpt)}</p>
    </main>
  );
}

Öffnen Sie die DevTools: entweder direkt im ChatGPT-Fenster (über den eingebauten Tunnel-Viewer, falls verfügbar) oder im lokalen Browser beim direkten Öffnen der Seite. Vergleichen Sie in beiden Varianten:

  • beim Start im normalen Browser ist isChatGptApp false, und window.openai ist höchstwahrscheinlich undefined;
  • beim Start über ChatGPT sehen Sie ein Objekt mit Feldern wie toolInput, toolOutput, theme usw.

Das vermittelt ein gutes Bauchgefühl: derselbe React-Code verhält sich je nach Umgebung unterschiedlich, und genau dafür gibt es die Hooks.

Übung: „Alles ausgeben, was die Plattform liefert“

Fügen Sie eine temporäre Debug-Komponente hinzu:

import { useWidgetProps, useOpenAIGlobal } from "../hooks/openai";

export function DebugPanel() {
  const { toolInput, toolOutput, toolResponseMetadata } = useWidgetProps();
  const theme = useOpenAIGlobal("theme");
  const displayMode = useOpenAIGlobal("displayMode");

  return (
    <pre style={{ fontSize: 10, maxHeight: 200, overflow: "auto" }}>
      {JSON.stringify(
        { toolInput, toolOutput, toolResponseMetadata, theme, displayMode },
        null,
        2
      )}
    </pre>
  );
}

Und fügen Sie vorübergehend <DebugPanel /> unter dem Haupt-UI ein. So sehen Sie anschaulich:

  • welche Felder genau aus dem MCP in toolOutput kommen;
  • was in _meta steckt (z. B. locale, userLocation und anderes);
  • wie sich displayMode ändert, wenn Sie das Widget vergrößern.

Später können Sie die Komponente entfernen oder per Flag wie DEBUG_WIDGET ein- und ausschalten.

6. Beziehungen: ChatGPT ↔ Widget ↔ MCP/Server

Damit man das Widget nicht als „Hauptakteur“ des Systems missversteht, lohnt es sich, die Rollen noch einmal festzuhalten.

  1. Der Nutzer schreibt eine Nachricht: „Finde ein Geschenk für meine Freundin, Budget 50 $“.
  2. Das ChatGPT-Modell entscheidet, Ihr MCP-Tool search_gifts mit den Argumenten { recipient: "girlfriend", budget: 50 } aufzurufen.
  3. Der MCP-Server führt die Business-Logik aus und liefert:
    • content mit einer kurzen Beschreibung fürs Modell;
    • structuredContent mit einer Liste von Geschenken;
    • _meta mit technischen Details (z. B. Quelle und Währung).
  4. ChatGPT:
    • zeigt dem Nutzer eine Textnachricht („Ich habe einige Optionen gefunden …“);
    • erstellt das Widget-iframe und übergibt structuredContent und _meta an window.openai.toolOutput und toolResponseMetadata.
  5. Ihr Widget:
    • rendert das UI anhand von toolOutput;
    • ruft bei Interaktionen callTool auf oder sendet Follow-ups;
  6. Das Modell entscheidet anschließend, was mit den Ergebnissen zu tun ist.

Das führt zu einer wichtigen Erkenntnis: Das Widget ist nie der alleinige Taktgeber des Prozesses. Es ist die UI-Schicht, die in einem Ökosystem aus Modell und MCP-Server lebt. Komplexe Dinge (Authentifizierung, Zugriff auf private Daten, ernsthafte Business-Logik) sollten auf der Serverseite bleiben. Das Widget ist für eine bequeme Oberfläche und die saubere Kommunikation mit dem Nutzer verantwortlich.

7. Richtlinien und Spielregeln in der Sandbox

Das ganze Konstrukt mit isoliertem iframe und window.openai existiert aus Gründen von Sicherheit und Privatsphäre. Die offiziellen OpenAI-Guides betonen mehrere Prinzipien.

Erstens: Datenminimierung. Sie sollten nicht versuchen, dem Nutzer über das Widget so viel PII (personally identifiable information) wie möglich zu entlocken und zu sich zu senden. Alles, was wirklich nötig ist, muss klar in den Tools beschrieben sein, und Modell sowie Security-Layer schauen sich solche Aufrufe genau an.

Zweitens: Verbot von verstecktem Tracking und Fingerprinting. Es ist nicht erlaubt, ein heimliches Beobachtungssystem für das Gerät des Nutzers zu bauen, Browser-Fingerabdrücke zu sammeln oder Einschränkungen zu umgehen. Parameter wie userAgent, userLocation usw. sind Hinweise für den UX-Kontext, nicht für Authentifizierung oder Identifikation.

Drittens: Alles, was Sie in structuredContent, _meta, widgetState packen, kann der Nutzer in irgendeiner Form sehen oder ein Store-Reviewer kann es einsehen. Deshalb:

  • keine API-Keys, Tokens, Passwörter und Admin-Geheimnisse dort ablegen;
  • den Widget-Zustand so gestalten, dass Nutzer nicht überrascht wären, ihn in Logs oder Debug-Ausgaben zu sehen.

Viertens: Netzwerkaufrufe. Direkte Requests aus dem Widget zu Drittanbieter-APIs sind nur zu einem streng begrenzten Domain-Set und in nicht-sensiblen Szenarien zulässig. Sobald es um Geld, Accounts oder private Daten geht — alles über MCP/Backend abwickeln.

8. Häufige Fehler bei der Arbeit in der Sandbox und mit window.openai

Fehler Nr. 1: annehmen, das Widget sei „eine gewöhnliche Website in einem iframe“.
Anfänger versuchen reflexartig, in window.parent zu greifen, die Styles von ChatGPT zu ändern oder localStorage wie immer zu nutzen. In der Sandbox funktioniert das entweder nicht oder instabil: andere Origin, isolierter Storage, blockierter DOM-Zugriff. Akzeptieren Sie, dass Sie in einer verwalteten Umgebung leben und nur über window.openai und Hooks mit dem Host sprechen.

Fehler Nr. 2: window.openai überall direkt anfassen.
Code der Form window.openai.toolOutput in zehn Komponenten führt zu einer schwer zu debuggenden App. Sie müssen dann selbst auf Events hören, Asynchronität beachten und undefined prüfen. Zuverlässiger ist es, direkt useWidgetProps, useWidgetState, useOpenAIGlobal und andere Hooks zu verwenden, die openai:set_globals bereits kapseln und den Zustand synchronisieren.

Fehler Nr. 3: alles (vor allem Secrets) in widgetState speichern.
Man ist versucht, „vorsichtshalber“ ein riesiges Objekt mit API-Ergebnissen oder gar Access-Token dort abzulegen. Das vergrößert den Kontext, verschlechtert die Arbeit des Modells und verstößt gegen grundlegende Sicherheitsanforderungen. widgetState sollte klein sein, nur UI-Signale enthalten und niemals vertrauliche Daten.

Fehler Nr. 4: direkt aus dem Widget ins Internet gehen.
Aufrufe wie fetch("https://api.superbank.com/...") aus der Sandbox stoßen fast garantiert auf CORS, und selbst wenn alles perfekt konfiguriert ist, bleibt es unsicher und schwer kontrollierbar. Alles, was mit echten Accounts, Geld und personenbezogenen Daten zu tun hat, sollte als MCP-Tools umgesetzt und über callTool oder die Serverseite aufgerufen werden.

Fehler Nr. 5: sich auf die Stabilität von window.openai außerhalb von ChatGPT verlassen.
Manche Entwickler starten das Widget als eigenständiges SPA und prüfen nicht, dass window.openai undefined sein kann. Im Dev-Umfeld endet das mit „Cannot read properties of undefined“. Verwenden Sie useIsChatGptApp, prüfen Sie auf typeof window !== "undefined" und bauen Sie ein Fallback-UI für Fälle ohne echtes Widget.

Fehler Nr. 6: den Umgebungskontext ignorieren (theme, displayMode, maxHeight, locale).
Man kann natürlich starr 2000px Höhe setzen, immer Dark Theme erzwingen und nur für Desktop layouten — aber das macht das Nutzungserlebnis seltsam. Die Plattform liefert Signale zu Platz, Theme und Locale — nutzen Sie sie über useOpenAIGlobal, useDisplayMode, useMaxHeight u. a., damit das Widget in ChatGPT „native“ wirkt.

Fehler Nr. 7: versuchen, die Policy über fremde Skripte zu umgehen.
Mitunter lockt die Idee, einen Tracker, ein fremdes JS-Bundle oder Code von einer anderen Domain „still“ nachzuladen. Die Sandbox und CSP-Policies sind genau dafür da, das zu verhindern: Fremdskripte werden blockiert, und solche Umgehungsversuche führen direkt zur Ablehnung Ihrer App im Store.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION