Écrire ses propres extensions personnelles

Ce guide explique comment écrire une Browser Extension ou une Server Extension, en faire un package et l'importer. Il s'adresse aux personnes qui savent écrire du JavaScript. Pour examiner et exécuter une extension, consulte Extensions personnelles.

Le code que tu écris et importes toi-même est traité comme une External Extension (extension externe). Il est désactivé au départ et ne peut pas s'exécuter tant que tu ne l'as pas examiné et que tu n'as pas approuvé son empreinte SHA-256 exacte.

Avant de commencer

Les External Extensions restent masquées tant que tu n'as pas franchi les deux étapes de sécurité décrites dans Autoriser les imports tiers : ENABLE_EXTERNAL_EXTENSIONS=true dans .env, et Allow third-party extension imports (autoriser les imports d'extensions tierces) sous Settings → Advanced → Danger Zone (Paramètres → Avancé → Zone de danger).

L'importation et la gestion des extensions nécessitent aussi un accès localhost ou le secret d'administration. Depuis un téléphone, une adresse LAN ou un navigateur distant, définis ADMIN_SECRET sur le serveur et saisis la même valeur dans Settings → Advanced → Admin Access (Paramètres → Avancé → Accès administrateur).

Choisis l'environnement le moins puissant qui suffit à la tâche :

EnvironnementUsageLimite importante
Sandboxed Browser ExtensionÉtat privé, contexte du chat actif, boutons, actions de menu et panneaux rendus par MarinaraAucun accès au DOM de Marinara, aux cookies, au stockage du navigateur, au réseau ni à du HTML arbitraire
Server ExtensionLogique en arrière-plan qui nécessite des minuteurs gérés et un stockage privé pour l'extensionBac à sable distinct, géré par le système d'exploitation ; aucun accès aux fichiers ou secrets de Marinara, au réseau, aux processus enfants ni aux modules natifs
Full-page External ExtensionAncien code qui a besoin de la page de Marinara ou des API de même origineAucun bac à sable ; à réserver au code exact auquel tu fais pleinement confiance

Les Browser Extensions fonctionnent sur toutes les plateformes prises en charge. Les Server Extensions nécessitent Seatbelt sous macOS ou Bubblewrap sous Linux. Consulte le tableau des plateformes avant de choisir une Server Extension.

Démarrage rapide d'une Browser Extension

Crée un dossier organisé comme ceci :

Hello Panel/
  manifest.json
  extension.js
  extension.css

Utilise ce fichier manifest.json :

{
  "kind": "marinara.personal-extension",
  "version": 1,
  "config": {
    "name": "Hello Panel",
    "version": "1.0.0",
    "description": "A minimal sandboxed Browser Extension.",
    "runtime": "client",
    "capabilities": [],
    "jsPath": "extension.js",
    "cssPath": "extension.css"
  }
}

Utilise ce fichier extension.js :

const saved = await marinara.storage.get();
let count = Number(saved.count) || 0;

const statusElement = () => ({
  kind: "text",
  text: `Button pressed ${count} time${count === 1 ? "" : "s"}.`,
});
const elements = () => [
  statusElement(),
  { kind: "button", id: "increment", label: "Count one" },
];

const panel = marinara.ui.registerContribution({
  id: "hello-panel",
  kind: "panel",
  label: "Hello Panel",
  description: "Minimal Personal Extension example",
  icon: "hand",
  elements: elements(),
  onEvent: async ({ elementId }) => {
    if (elementId !== "increment") return;
    count += 1;
    await marinara.storage.patch({ count });
    panel.update({ elements: elements() });
    marinara.ui.showWindow({ title: "Hello Panel", elements: [statusElement()] });
  },
});

marinara.log.info("Hello Panel loaded");
marinara.onCleanup(() => panel.remove());

Utilise ce fichier extension.css pour mettre en forme la fenêtre iframe restreinte ouverte par le bouton :

[data-ext-root] {
  font-size: 16px;
}

Importe ensuite l'extension et exécute-la :

  1. Ouvre Settings → Addons → External Extensions (Paramètres → Modules complémentaires → Extensions externes).
  2. Choisis Import Extension Folder (importer un dossier d'extension) et sélectionne Hello Panel, ou compresse le dossier et importe le fichier ZIP avec Import Extension File (importer un fichier d'extension).
  3. Ouvre le brouillon désactivé et examine son manifeste et son code JavaScript.
  4. Choisis Review and Run (relire et exécuter) et approuve l'empreinte exacte affichée.
  5. Ouvre le menu Extensions et sélectionne Hello Panel.

Le même exemple exécutable se trouve dans le dépôt, sous docs/examples/personal-extensions/browser-minimal/.

Référence de la Browser API

Les Browser Extensions exécutées dans le bac à sable reçoivent un seul objet global figé nommé marinara :

APIRôle
runtime, versionNom de l'environnement (client) et version actuelle de la Browser API
extensionId, extensionName, capabilitiesIdentité et capacités approuvées pour cette révision exacte de l'extension
log.debug/info/warn/error(...)Écrire une entrée étiquetée dans la console du navigateur
storage.get()Lire l'objet JSON privé de cette extension
storage.patch(object)Fusionner des valeurs dans le stockage privé et renvoyer le nouvel objet
storage.delete()Effacer le stockage privé
context.get()Lire l'instantané actuel du chat actif
context.subscribe(listener)Recevoir les changements de contexte ; renvoie une fonction de désabonnement
ui.registerContribution(options)Ajouter un bouton sûr, un élément du menu Extensions ou un panneau rendu par Marinara
ui.showWindow(options)Ouvrir une fenêtre iframe restreinte
dice.registerFormat(options)Apprendre une notation à /roll ; Marinara lance les dés qu'elle désigne
setTimeout, setInterval, clearTimeout, clearIntervalMinuteurs gérés et supprimés à l'arrêt de l'extension
onCleanup(callback)Enregistrer une logique de nettoyage supplémentaire

Utilise les panneaux rendus par Marinara pour l'interface normale, le contexte du chat actif pour les comportements qui dépendent du chat et les formats de dés pour une notation /roll personnalisée. L'état de l'extension doit rester dans marinara.storage, pas dans le stockage du navigateur.

showWindow({ title, elements, onEvent, onClose }) renvoie un descripteur doté de update({ title?, elements? }) et close(). Le CSS du package met en forme ces fenêtres iframe isolées ; les contributions rendues par l'hôte utilisent toujours le thème et les contrôles propres à Marinara.

L'environnement Browser sûr ne possède aucune API de DOM ou de réseau. Ne contourne pas cette limite. Si une capacité utile manque, demande une capacité précise et limitée à l'hôte au lieu d'adopter par défaut l'accès à la page entière.

Capacités de contexte

Déclare l'accès facultatif aux enregistrements dans config.capabilities :

{
  "capabilities": ["read_active_characters", "read_active_persona"]
}

Modifier les capacités change l'empreinte de l'exécutable, désactive l'extension et impose un nouvel examen.

Ce qu'une Browser Extension peut faire

Une Browser Extension s'exécute dans un Worker dédié, à l'intérieur d'une iframe isolée à origine opaque. Elle n'a accès ni à la page de Marinara, ni au DOM, ni aux cookies, ni au stockage du navigateur, ni aux API d'origine, ni au réseau. Ses capacités se limitent à un stockage d'extension privé, à la journalisation, à des minuteurs gérés, à l'enregistrement d'un nettoyage, à des fenêtres restreintes, à des emplacements de contribution sûrs dans l'interface hôte et à un instantané en lecture seule des identifiants du chat actif et des personnages. Elle ne reçoit certains champs des fiches de personnage actives ou du persona sélectionné que si les permissions correspondantes sont déclarées et approuvées.

Une extension peut ajouter des actions dans la barre supérieure ou dans un panneau latéral, des entrées au menu Extensions et des panneaux persistants sur le côté droit, via marinara.ui.registerContribution(...). Marinara affiche ces surfaces avec le thème actif et un jeu fixe de contrôles : titres, texte, sortie préformatée, boutons, champs de saisie de texte, listes de sélection, interrupteurs, curseurs, sélecteurs de couleur et espaceurs. Une extension fournit du contenu et un état, jamais de HTML, de CSS, d'URL, de composants React ni de gestionnaires d'événements de l'hôte.

Ces capacités d'interface et ces règles sont identiques pour toutes les Browser Extensions en bac à sable, quelle que soit leur provenance. Une extension tierce importée (External Extension) utilise cet environnement sûr, sauf si son package demande explicitement Full page access (accès complet à la page) ou emploie le format marinara.extension antérieur au bac à sable, décrit plus bas.

Estimer les tokens d'un texte

Les extensions personnelles Browser, Full page access et Server peuvent utiliser l'estimateur de tokens de texte intégré à Marinara :

const tokens = marinara.estimateTextTokens(text);

La signature est estimateTextTokens(text: string): number, également décrite par le type exporté PersonalExtensionTokenApi de @marinara-engine/shared. L'appel est synchrone, ne nécessite aucune capacité supplémentaire et renvoie l'estimation de tokens de Marinara, indépendante du modèle, plutôt que le résultat exact d'un tokenizer. Sur une ancienne version d'Engine, vérifie typeof marinara.estimateTextTokens === "function" avant d'appeler la fonction.

Ajouter un panneau rendu par Marinara

const panel = marinara.ui.registerContribution({
  id: "weather-settings",
  kind: "panel",
  label: "Weather controls",
  description: "Tune a weather scene without leaving Marinara.",
  icon: "sparkles",
  elements: [
    { kind: "heading", text: "Atmosphere" },
    {
      kind: "select",
      id: "weather",
      label: "Weather",
      value: "rain",
      options: [
        { value: "rain", label: "Rain" },
        { value: "snow", label: "Snow" },
        { value: "aurora", label: "Aurora" },
      ],
    },
    { kind: "slider", id: "intensity", label: "Intensity", min: 0, max: 100, value: 60 },
    { kind: "toggle", id: "lightning", label: "Lightning", checked: false },
    { kind: "color", id: "tint", label: "Tint", value: "#6d8cff" },
    { kind: "button", id: "apply", label: "Apply" },
  ],
  onActivate: async () => {
    const settings = await marinara.storage.get();
    // Update the panel when stored state should be reflected in the controls.
  },
  onEvent: async ({ elementId, values }) => {
    if (elementId !== "apply") return;
    await marinara.storage.patch(values);
  },
});

marinara.onCleanup(() => panel.remove());

Utilise kind: "button" pour une action compacte et kind: "menu-item" pour une action du menu Extensions. Utilise kind: "command" pour une entrée du Command Center (centre de commandes) qui active une contribution existante de la même révision de l'extension. Une commande exige targetContributionId et ne peut utiliser ni onActivate ni onEvent.

marinara.ui.registerContribution({
  id: "open-weather",
  kind: "command",
  label: "Open weather controls",
  targetContributionId: "weather-settings",
});

L'hôte ne résout une commande que si la cible a le même identifiant d'extension et la même empreinte de contenu. Les cibles absentes, issues d'une autre extension ou d'une révision périmée échouent par sécurité. Par défaut, les boutons ciblent surface: "top-bar". Ils peuvent aussi viser chats, bots, characters, personas, lorebooks, presets, connections, agents ou settings, avec position réglé sur header, before-content ou after-content. icon accepte tout nom d'icône Lucide en kebab-case pris en charge par Marinara. Les deux types d'action appellent onActivate. Un panel appelle onActivate à son ouverture ; ses boutons appellent onEvent avec les valeurs actuelles de tous les contrôles du panneau. Le descripteur renvoyé permet des mises à jour propres à chaque type : button accepte label, description, icon, surface et position ; menu-item accepte label, description et icon ; panel accepte label, description, icon et elements. Tous les descripteurs prennent en charge remove(). Les identifiants peuvent contenir des lettres, des chiffres, ., _ et -.

Par exemple, ceci place une action native au-dessus du contenu du panneau Presets :

marinara.ui.registerContribution({
  id: "preset-helper",
  kind: "button",
  label: "Preset helper",
  description: "Run the preset helper",
  icon: "list-sparkles",
  surface: "presets",
  position: "before-content",
  onActivate: () => {
    // Run extension behavior here.
  },
});

Un outil complexe peut construire une interface en plusieurs étapes en mettant à jour les éléments du panneau après un événement. Garde l'état de l'application dans marinara.storage ; ne l'encode pas dans le balisage.

Utiliser le contexte du chat actif

La version 5 de la Browser Extension API expose des identifiants opaques pour le chat affiché à l'instant dans Marinara :

const renderForContext = async ({ chatId, characterId, characterIds, personaId, characters, persona }) => {
  if (!chatId) return; // Home, a library, or another surface without an active chat.

  const storage = await marinara.storage.get();
  const tab = storage.tabsByChat?.[chatId];

  // characterId is available only for a single-Character chat.
  // Use characterIds for group chats.
  marinara.log.debug("Loaded Notepad tab", {
    chatId,
    characterId,
    characterIds,
    personaId,
    characterNames: characters.map((character) => character.name),
    personaName: persona?.name ?? null,
    tab,
  });
};

const unsubscribe = marinara.context.subscribe(renderForContext);
marinara.onCleanup(unsubscribe);

marinara.context.get() renvoie le même instantané actuel, sans abonnement. Quand aucun chat n'est actif, chatId vaut null et characterIds est vide. characterId n'est renseigné que si exactement un personnage participe ; dans un chat de groupe, characterIds expose tous les participants et characterId reste à null. personaId n'est renseigné que si la permission read_active_persona est approuvée.

Les identifiants de chat et de personnage sont toujours disponibles et permettent à une extension de cloisonner son propre stockage privé. Les champs des enregistrements exigent une des deux permissions facultatives du manifeste de l'extension, ou les deux :

{
  "runtime": "client",
  "capabilities": ["read_active_characters", "read_active_persona"]
}

Sans la permission, sa valeur reste [] ou null. Marinara affiche chaque permission demandée dans Requested access (accès demandés), puis à nouveau dans la boîte de dialogue d'approbation de l'empreinte exacte. Ajouter ou retirer une permission change l'empreinte de l'exécutable, désactive l'extension et impose une nouvelle approbation.

Un instantané de personnage ne contient que id, name, description, personality, scenario, firstMessage, exampleDialogue, creator, characterVersion, tags, backstory, appearance, aboutMe et conversationDisplayName. Un instantané de persona ne contient que id, name, description, personality, scenario, backstory, appearance, tags, aboutMe et conversationDisplayName. Le texte est limité en longueur avant de franchir le pont du bac à sable.

Marinara n'envoie jamais les messages, les notes du créateur, les prompts système, les instructions post-historique, les commentaires, les chemins des avatars, les bibliothèques complètes de personnages ou de personas, les champs non déclarés, les métadonnées du chat, les descripteurs de base de données, un accès réseau ni des opérations de modification. Les mises à jour du contexte restent liées à l'empreinte approuvée du code ; elles arrivent quand le chat actif, sa liste de personnages ou son persona sélectionné change.

Ajouter un format de dés

La version 6 de la Browser Extension API permet à une extension d'apprendre à /roll une notation qui lui est propre, comme une réserve de dés narratifs. L'extension lit la notation et désigne les dés à lancer ; Marinara les lance avec son propre générateur sécurisé et lui renvoie les faces. Une extension peut dire ce que signifie un jet, jamais ce que les dés ont donné.

const SYMBOLS = { boost: [0, 0, 1, 1, 2, 1], setback: [0, 0, -1, -1, 0, 0] };

const format = marinara.dice.registerFormat({
  id: "narrative",
  label: "Narrative dice",
  // Return null for notation this format does not read.
  parse(notation) {
    const terms = [...notation.matchAll(/(\d+)\s*(boost|setback)/g)];
    if (!terms.length) return null;
    const kinds = terms.flatMap(([, count, kind]) => Array(Number(count)).fill(kind));
    return { dice: kinds.map(() => 6), kinds };
  },
  // rolls[i] is the face Marinara threw for dice[i]; plan is what parse returned.
  read({ notation, dice, rolls, plan }) {
    const net = rolls.reduce((sum, face, i) => sum + SYMBOLS[plan.kinds[i]][face - 1], 0);
    return { total: net, text: net > 0 ? `${net} success` : "failure" };
  },
});
marinara.onCleanup(() => format.remove());

/roll 2 boost 1 setback publie alors les faces, le total et le texte de l'extension, avec le nom du format comme libellé. Une notation simple comme 2d6 ou 4d6kh3 lance toujours les dés comme avant et n'est jamais transmise à une extension. Les autres notations sont transmises à chaque format enregistré à tour de rôle ; le premier dont parse renvoie des dés effectue le jet.

Les formats de dés s'appliquent à /roll dans le navigateur. L'outil roll_dice du Game Master et les autres jets côté serveur conservent la notation intégrée et les dés de l'ensemble de règles. Sur une ancienne version d'Engine, vérifie typeof marinara.dice?.registerFormat === "function" avant de l'appeler.

Anciennes extensions et extensions pleine page

Contrôleurs météo, éditeurs de prompts et autres flux de travail conséquents sont des cas d'usage valables pour les contributions. Leur portage sûr peut s'appuyer sur un lanceur dans le menu ou la barre supérieure, complété par des panneaux mis à jour au fil des étapes. Les packages existants qui superposent des éléments au DOM, interrogent les sélecteurs CSS de Marinara, parcourent les entrailles de React ou appellent les routes /api de même origine ne peuvent pas être importés tels quels dans l'environnement sûr.

Les contributions d'interface fournissent l'interface, pas des pouvoirs implicites. L'API de contexte expose toujours les identifiants du chat actif et des personnages ; au-delà, elle ne peut exposer que les champs déclarés des enregistrements actifs listés plus haut. Une fonctionnalité qui a besoin des messages, des presets, des lorebooks, de données de personnage ou de persona non déclarées, ou d'effets visuels de scène réclame toujours une capacité de courtage distincte et étroitement délimitée, exposée par Marinara. Une extension ne doit pas la simuler en accédant au DOM de l'hôte ou en lançant des requêtes réseau sans restriction.

Si une External Extension dépend d'un accès au DOM de l'hôte, elle peut demander :

{
  "runtime": "client",
  "capabilities": ["full_page_access"]
}

Full page access n'est pas une capacité du bac à sable. Le JavaScript et le CSS approuvés s'exécutent à l'intérieur de la page de Marinara. Ce code peut lire ou modifier tout ce qui est visible dans la session de navigateur en cours, inspecter les chats et les fiches, utiliser le stockage du navigateur, lancer des requêtes réseau et appeler les API de Marinara de même origine. Ses pouvoirs sur la page équivalent en pratique à du code collé dans la console du navigateur. Les brouillons de Professor Mari ne peuvent pas le demander.

Les extensions pleine page devraient utiliser marinara.fetch(...) pour les requêtes réseau. Cette fonction a la même signature et renvoie le même résultat que window.fetch, tout en permettant à Settings > Addons > External Extensions d'afficher le nombre de requêtes de cette extension pendant la session, les octets transférés indiqués par les réponses, la fréquence récente des requêtes et un avertissement en cas de trafic élevé prolongé. Les appels directs à window.fetch restent disponibles, car l'accès à la page entière exécute du code de confiance dans la page, mais ces requêtes ne peuvent pas être attribuées à l'extension.

Marinara traite l'ancienne enveloppe v1 kind: "marinara.extension", dépourvue de champ capabilities explicite, comme un package antérieur au bac à sable et lui attribue Full page access à l'importation. Les packages anciens comme WeatherTweaker rejoignent ainsi le bon parcours d'examen, au lieu d'échouer en silence dans un Worker. Un package moderne qui emploie cette enveloppe mais veut l'environnement sûr doit inclure "capabilities": [].

Les deux protections des External Extensions et l'approbation de l'empreinte exacte restent en vigueur. Toute modification du code, du CSS ou des permissions désactive l'extension et impose une nouvelle approbation. La désactivation supprime les nœuds de script et de feuille de style posés par Marinara, annule les minuteurs créés via l'API de compatibilité et exécute les rappels enregistrés avec marinara.onCleanup(...). Comme le code de la page peut créer des écouteurs, des minuteurs, des variables globales ou des modifications du DOM non enregistrés, le nettoyage se fait au mieux : recharge la page après avoir désactivé une extension s'il en reste quelque chose.

L'ancienne API marinara.ui.showWindow(...) reste disponible pour ouvrir une fenêtre temporaire dans l'iframe à origine opaque. Elle emploie les mêmes contrôles fixes et renvoie les descripteurs update(...) et close(). Privilégie les contributions quand l'outil doit rester accessible par la navigation habituelle de Marinara.

Démarrage rapide d'une Server Extension

Crée ce dossier :

Server Counter/
  manifest.json
  server-extension.js

Utilise ce fichier manifest.json :

{
  "kind": "marinara.personal-server-extension",
  "version": 1,
  "config": {
    "name": "Server Counter",
    "version": "1.0.0",
    "description": "A minimal sandboxed Server Extension.",
    "runtime": "server",
    "capabilities": [],
    "serverJsPath": "server-extension.js"
  }
}

Utilise ce fichier server-extension.js :

const saved = await marinara.storage.get();
const starts = (Number(saved.starts) || 0) + 1;
await marinara.storage.patch({ starts });

marinara.log.info(`Server Counter started ${starts} time${starts === 1 ? "" : "s"}`);

const timer = marinara.setInterval(() => {
  marinara.log.debug("Server Counter heartbeat");
}, 60_000);

marinara.onCleanup(() => marinara.clearInterval(timer));

Le même package exécutable est disponible sous docs/examples/personal-extensions/server-minimal/.

Le code serveur reçoit marinara.runtime, marinara.version, l'identité de l'extension, log, storage, les minuteurs gérés et onCleanup. Il ne reçoit aucun accès au système de fichiers, aux processus, au réseau, au chargement de modules ou à la base de données de Marinara.

Les Server Extensions restent désactivées lorsque l'hôte ne peut pas mettre en place Seatbelt ou Bubblewrap. Il s'agit d'une restriction de la plateforme, pas d'une erreur de l'extension.

Référence du package et du manifeste

ChampRemarques
kindmarinara.personal-extension ou marinara.personal-server-extension
version de premier niveauVersion de l'enveloppe du package ; actuellement 1
config.nameNom d'affichage obligatoire, de 1 à 200 caractères
config.versionVersion facultative de l'extension, par exemple 1.2.0 ; les versions numériques séparées par des points permettent les avertissements de rétrogradation
config.descriptionDescription facultative, jusqu'à 2 000 caractères
config.runtimeclient ou server ; valeur par défaut : client
config.capabilitiesCapacités Browser demandées ; les Server Extensions doivent utiliser une liste vide
config.jsPath / config.serverJsPathChemin du fichier JavaScript ou tableau ordonné de chemins, relatif au manifeste
config.cssPathChemin facultatif du fichier CSS ou tableau ordonné ; le CSS de l'environnement sûr reste dans l'iframe isolée
config.js, config.serverJs, config.cssVariantes en ligne lorsque des fichiers distincts sont inutiles

Utilise du JavaScript simple. Marinara ne compile pas TypeScript et n'installe pas les dépendances d'une extension. Si nécessaire, regroupe les dépendances dans ton JavaScript avant l'importation.

Les fichiers indépendants .js, .mjs, .cjs, .server.js, .server.mjs, .server.cjs et .css peuvent aussi être importés directement. Un manifeste reste préférable, car il enregistre explicitement l'identité, l'environnement, la version, les capacités et l'ordre des fichiers.

Limites de validation

ContenuLimite actuelle
Nom / version / description200 caractères / 64 caractères / 2 000 caractères
JS Browser ou ServerAucune limite de source par champ ; la limite du fichier, de l'archive ou de la requête englobante reste applicable
CSS256 KiB
ZIP importé32 MiB compressés, 2 MiB par entrée de texte et 16 MiB de texte extrait au total
Stockage privé1 000 000 octets de JSON sérialisé par extension

Les limites du ZIP, de la requête, des messages du bac à sable et du stockage protègent des frontières distinctes du transport ou de l'exécution ; elles ne constituent pas une règle sur le code source exécutable.

Cycle de mise à jour et de récupération

Débogage

SymptômeVérification
Les contrôles d'importation externe sont absentsOuvre les deux protections External Extension décrites plus haut
La gestion indique que localhost ou Admin Access est requisConfigure ADMIN_SECRET et enregistre-le sous Admin Access
L'importation ne trouve aucune extensionVérifie manifest.json et ses chemins relatifs ; Server nécessite du JS, tandis que Browser nécessite du CSS ou du JS
L'extension se désactive après une modificationC'est normal : examine et approuve la nouvelle empreinte exacte
Le code Browser ne peut pas utiliser document, window, fetch ou le stockage localC'est normal dans le bac à sable sûr ; utilise les API intermédiaires documentées
Server Extension est indisponibleUtilise Seatbelt sous macOS ou Linux avec Bubblewrap, ou passe à une Browser Extension
Browser Extension lève une exceptionOuvre les outils de développement du navigateur ; marinara.log et les erreurs de démarrage portent le nom de l'extension
Server Extension lève une exceptionVérifie son état dans Settings → Addons et le journal du serveur Marinara

Le CSS, le stockage privé, les archives importées et les messages d'exécution conservent des limites de sécurité distinctes. Marinara devrait signaler la limite qui a rejeté un package au lieu de présenter le problème comme un échec d'exécution.

Guides connexes