← Tous les articles
Développement applicatif Dirigeants métier · · Par ObjectStack Team

Comment laisser des visiteurs anonymes soumettre un formulaire dans une application métier gouvernée ? Une porte publique, large de cinq champs

Depuis la v17, l’accès anonyme aux données est toujours refusé. Seule porte ouverte : une vue de formulaire publique. Le droit vient du formulaire — cinq champs entrent, une ligne est créée, seul son id revient. Mesuré sur 18.3.0.

Comment laisser des visiteurs anonymes soumettre un formulaire dans une application métier gouvernée ? Une porte publique, large de cinq champs Comment laisser des visiteurs anonymes soumettre un formulaire dans une application métier gouvernée ? Une porte publique, large de cinq champs
  • Formulaires publics
  • Web-to-lead
  • Accès anonyme

En bref. Depuis la v17, une requête non authentifiée sur des données d’objet dans une application ObjectStack est refusée avec 401 UNAUTHENTICATED — chaque objet, chaque point d’accès, sans option de désactivation. La seule porte qu’une application gouvernée laisse ouverte au public est une vue de formulaire publique : on déclare un formulaire, on marque son bloc sharing avec allowAnonymous: true, et le runtime dérive toute l’autorisation du visiteur de cette déclaration — créer un enregistrement sur cet unique objet, rien d’autre, et la réponse est l’id du nouvel enregistrement. La liste des champs du formulaire est la frontière de sécurité. Le formulaire web-to-lead de l’exemple CRM liste cinq champs, donc cinq champs peuvent arriver ; les huit autres champs métier et toutes les colonnes système ne le peuvent pas, quoi que le visiteur envoie, et un champ que le formulaire marque obligatoire doit être rempli avant que quoi que ce soit ne soit écrit. J’appellerai cela un droit à la forme du formulaire (form-shaped grant) : une autorisation calculée à partir du formulaire au moment de la requête, et qui ne peut donc pas être configurée plus large que le formulaire. Tout ce qui suit a été mesuré sur la @objectstack/cli 18.3.0 publiée, la version que npm create objectstack@latest installe aujourd’hui — y compris la question de savoir s’il faut un ensemble de permissions guest_portal. Non, et depuis la 18.0 l’exemple livré le dit aussi.

Le problème, dans les mots de la documentation

Le guide ObjectStack sur la collecte publique de données s’ouvre sur le scénario tel qu’un utilisateur le formulerait :

Je veux que des visiteurs anonymes soumettent un formulaire — « Contactez-nous », « Demander une démo », un formulaire d’admission — qui crée un enregistrement. Mais je ne dois pas leur donner accès à la base, et ils ne doivent pouvoir renseigner que les champs du formulaire.

Toute application métier en a un : le formulaire de leads sur le site, l’admission du support, la demande de démo, la réclamation de garantie. Les réponses habituelles ont chacune un coût. Un rôle « invité » avec droit d’écriture sur la table est un droit permanent que personne ne relit, et il dérive. Un point d’accès écrit à la main contourne le modèle de permissions qui gouverne tout le reste. Un outil de formulaires tiers avec un webhook signifie que deux systèmes détiennent désormais le schéma, et l’un d’eux n’est pas celui que vos relecteurs regardent.

Depuis la v17, la première réponse n’est même plus disponible. La documentation des permissions en note la raison au passage : « since v17 anonymous access to object data is always denied anyway — the requireAuth: false opt-out was retired » (depuis la v17, l’accès anonyme aux données d’objet est de toute façon toujours refusé — l’option requireAuth: false a été retirée). Le guide d’auto-hébergement en fait une étape de vérification — faire un curl sur un point d’accès d’objet sans identifiants et exiger le 401 avec un code lisible par machine — et traite un 200 à cet endroit comme un incident. La forme honnête de la question est donc : si le plan de données est fermé aux appelants anonymes par construction, comment un formulaire public passe-t-il ?

Un formulaire public est une déclaration, pas une permission

Voici l’intégralité du formulaire web-to-lead de l’exemple CRM, tel qu’il est livré au tag @objectstack/[email protected] :

formViews: {
  web_to_lead: {
    type: 'simple',
    data: { provider: 'object', object: 'crm_lead' },
    sections: [
      {
        name: 'contact_us',
        label: 'Contact us',
        columns: 1,
        fields: [
          { field: 'name', required: true },
          { field: 'company' },
          { field: 'email', required: true },
          { field: 'phone' },
          { field: 'title' },
        ],
      },
    ],
    sharing: {
      enabled: true,
      allowAnonymous: true,
      publicLink: '/forms/contact-us',
    },
  },
},

Rien n’y mentionne une permission. C’est le bloc sharing qui ouvre la porte : avec enabled, allowAnonymous et un publicLink définis, le runtime enregistre deux points d’accès anonymes — GET /api/v1/forms/contact-us, qui renvoie le formulaire résolu et le schéma filtré, et POST /api/v1/forms/contact-us/submit, qui insère un crm_lead. La règle d’une ligne de la documentation sur ce que le visiteur peut écrire est : « Only the fields listed in sections[].fields are accepted on submit; everything else is stripped server-side » (seuls les champs listés dans sections[].fields sont acceptés à la soumission ; tout le reste est retiré côté serveur). La phrase suivante, ajoutée en 18.0, dit que la route de soumission « also enforces each entry’s required: true before anything is written » (applique aussi le required: true de chaque entrée avant d’écrire quoi que ce soit). Et la raison qu’elle donne pour cette conception est la phrase que cet article existe pour mettre à l’épreuve : « Authorization is derived from the declaration, not configured separately — so the grant can’t drift wider than the form » (l’autorisation est dérivée de la déclaration, pas configurée à part — le droit ne peut donc pas dériver plus large que le formulaire).

Deux filets : la route ne garde que les cinq champs déclarés, écarte les noms gérés par le serveur et refuse un champ obligatoire vide ; la couche de données n’admet que l’insertion et la relecture sur crm_lead et retire à nouveau les mêmes noms. Le visiteur reçoit le nouvel id ; les lectures répondent 401 ; un formulaire retiré répond 404.

Mécaniquement, il y a deux filets, et ils méritent d’être nommés parce qu’un relecteur peut vérifier chacun.

Le premier filet est la route. Le gestionnaire de soumission construit sa liste d’autorisation à partir des sections[].fields du formulaire trouvé et ne copie que ces clés depuis le corps de la requête. Avant cela, il écarte par leur nom toutes les clés de PUBLIC_FORM_SERVER_MANAGED_FIELDS, un ensemble que le paquet spec exporte : id, owner_id, owning_business_unit_id, queue, organization_id, tenant_id, created_at, created_by, updated_at, updated_by, is_deleted, deleted_at et __search — treize noms qui, selon la documentation, sont « never accepted from a public submission — not even when a section declares them » (jamais acceptés depuis une soumission publique — pas même quand une section les déclare). (queue est le plus récent ; la 18.2 en avait douze.) Un formulaire sans aucun champ est refusé avec 400 au lieu d’accepter tout ce qui arrive. Ensuite, la route juge le formulaire lui-même, avant d’écrire quoi que ce soit : un champ que le formulaire marque required et affiche toujours doit porter une valeur (depuis la 18.0), une valeur dans un champ fichier est refusée (depuis la 18.2), tout comme une valeur hors du sous-ensemble d’options qu’offre une entrée du formulaire (depuis la 18.3) — le tout dans un seul 400 VALIDATION_FAILED qui nomme chaque champ.

Le second filet est la couche de données. La route attache à l’écriture un publicFormGrant: { object: 'crm_lead' } limité à la requête, et le plugin de sécurité honore exactement cela : insert, plus find, findOne et count pour la relecture immédiate, sur cet objet seulement. Avant d’admettre l’insertion, il retire une seconde fois les mêmes treize noms, si bien que même si un formulaire déclarait owner_id dans une section, ou si le filtre de la route et la couche de données étaient un jour modifiés séparément, un visiteur ne pourrait falsifier ni la propriété ni le rattachement à un locataire. Le droit est la forme du formulaire, lue au moment de la requête. Aucune ligne d’une table de permissions ne dit « l’anonyme peut écrire des leads », parce qu’il n’y a rien à configurer. La relecture atteint la route, pas le visiteur : la route répond avec l’id créé et rien d’autre de ce que l’insertion a stocké.

La transcription, sur 18.3.0

J’ai créé une application neuve avec npm create objectstack@latest — @objectstack/cli 18.3.0 — et j’y ai copié, depuis le tag @objectstack/[email protected], l’objet crm_lead de l’exemple CRM (avec les deux objets que nomment ses lookups), son lead.view.ts avec le formulaire web-to-lead, et son ensemble de permissions guest_portal. Puis objectstack dev, qui compile la configuration au démarrage et crée un compte administrateur sur une base vide. Chaque réponse ci-dessous est réelle et n’a été raccourcie que pour la longueur.

1. La porte se décrit elle-même — large de cinq champs. Sans identifiants :

curl -s http://localhost:49900/api/v1/forms/contact-us
{ "slug": "contact-us", "object": "crm_lead",
  "form": { "sections": [ { "name": "contact_us", "label": "Contact us", "fields": [
      { "field": "name", "required": true }, { "field": "company" },
      { "field": "email", "required": true }, { "field": "phone" }, { "field": "title" } ] } ],
    "sharing": { "enabled": true, "publicLink": "/forms/contact-us", "allowAnonymous": true } },
  "objectSchema": { "name": "crm_lead", "label": "Lead", "fields": {
      "name":    { "label": "Lead Name", "type": "text",  "required": true, "maxLength": 200 },
      "email":   { "label": "Email",     "type": "email", "required": false },
      "phone":   { "label": "Phone",     "type": "phone", "required": false },
      "company": { "label": "Company",   "type": "text",  "required": false, "maxLength": 200 },
      "title":   { "label": "Title",     "type": "text",  "required": false, "maxLength": 120 } } } }

L’objectSchema porte exactement les cinq champs autorisés, chacun avec le libellé, le type et les limites dont un front-end a besoin pour le dessiner. crm_lead déclare treize champs métier dans sa définition d’objet ; le schéma public en montre cinq et ne dit rien de status, lead_score, assigned_to, account, notes ni du reste. Le visiteur ne peut même pas apprendre quoi essayer. Les libellés suivent la langue du visiteur : avec le paquet de traduction chinois du CRM chargé, le même appel envoyé avec Accept-Language: zh-CN a servi en chinois l’en-tête de section, le libellé de l’objet et les cinq libellés de champ, sous Vary: Accept-Language.

2. Une soumission propre crée une ligne, et la réponse est son id.

curl -s -X POST http://localhost:49900/api/v1/forms/contact-us/submit \
  -H 'content-type: application/json' \
  -d '{"name":"Ada Lovelace","email":"[email protected]"}'
HTTP 201
{ "id": "dgYZFAvljruviVy9" }

C’est toute la réponse : l’id du nouvel enregistrement, et rien de ce que l’insertion a stocké. Relue en tant qu’administrateur, la ligne montre ce que le visiteur n’a pas envoyé :

curl -s -b admin.cookies http://localhost:49900/api/v1/data/crm_lead/dgYZFAvljruviVy9
{ "id": "dgYZFAvljruviVy9", "name": "Ada Lovelace", "email": "[email protected]",
  "status": "new", "source": "web", "lead_score": 0, "owner_id": null, "queue": null,
  "organization_id": "org_mw8n9qe618ypwdpk", "created_by": null }

status: "new" n’a pas été envoyé ; c’est la valeur par défaut de l’objet sur le chemin d’écriture, que l’objet lead déclare pour qu’« a create that omits status (e.g. the public Web-to-Lead form, which doesn’t collect it) is stored as ‘new’, so it satisfies required » (une création qui omet status — par exemple le formulaire public Web-to-Lead, qui ne le collecte pas — soit stockée comme ‘new’ et satisfasse ainsi required). source: "web" et lead_score: 0 sont aussi des valeurs par défaut. Sur 17.7.0, le 201 renvoyait toute cette ligne à l’appelant anonyme. Je l’ai signalé, et depuis la 18.0 la réponse n’est plus que l’id (issue objectstack #22437, corrigée par la PR #22462).

3. Une soumission hostile crée la même ligne. Six clés envoyées ; deux sont sur le formulaire :

curl -s -X POST http://localhost:49900/api/v1/forms/contact-us/submit \
  -H 'content-type: application/json' \
  -d '{"name":"Mallory","email":"[email protected]",
       "status":"converted","lead_score":100,
       "owner_id":"attacker","organization_id":"org_other"}'
HTTP 201
{ "id": "R2Wkp0koxTAw53tN" }

La réponse a la même forme qu’avant, elle ne dit donc rien de ce qui a été gardé. La relecture de cet id en tant qu’administrateur le dit :

{ "id": "R2Wkp0koxTAw53tN", "name": "Mallory", "email": "[email protected]",
  "status": "new", "lead_score": 0, "owner_id": null, "queue": null,
  "organization_id": "org_mw8n9qe618ypwdpk" }

Les deux champs autorisés sont arrivés, les deux champs métier absents du formulaire ont été écartés et les valeurs par défaut de l’objet se sont appliquées, et les deux ancres système ont été écartées puis tamponnées par le serveur — organization_id est l’organisation propre au déploiement, pas org_other, et owner_id vaut null. Lors d’un second démarrage, j’ai ajouté queue, id et created_by au même corps ; la ligne stockée avait un id généré par le serveur et null dans les deux autres. Le 201 ne dit pas quelles clés ont été refusées ; la route les écarte sans commentaire.

Clé envoyéeSur le formulaire ?Stockée comme (relecture admin)Pourquoi
name, emailouitelle qu’envoyéeautorisée
status: "converted"non"new"absente de sections[].fields ; la valeur par défaut de l’objet s’applique
lead_score: 100non0absente de sections[].fields ; la valeur par défaut de l’objet s’applique
owner_id: "attacker"nonnullnom géré par le serveur, refusé par les deux filets
organization_id: "org_other"noncelle du serveurnom géré par le serveur ; tamponné par le runtime
queue, id, created_by (second démarrage)nonnull, un id généré par le serveur, nullnoms gérés par le serveur

4. Un champ obligatoire manquant est refusé avant l’écriture. Le formulaire marque email obligatoire ; l’objet, non.

curl -s -X POST http://localhost:49900/api/v1/forms/contact-us/submit \
  -H 'content-type: application/json' \
  -d '{"name":"No Email"}'
HTTP 400
{ "error": "Email is required", "code": "VALIDATION_FAILED",
  "fields": [ { "field": "email", "code": "required", "message": "Email is required", "label": "Email" } ] }

Un email vide " " a reçu la même réponse, et un corps vide a nommé les deux champs dans une seule enveloppe ("Lead Name is required; Email is required"). Sur 17.7.0, le même POST répondait 201 avec email: null, parce que sur ce chemin seul le required de l’objet était appliqué. Je l’ai signalé aussi, et depuis la 18.0 la route applique d’abord le required propre au formulaire (issue objectstack #24131, PR #24161).

5. La porte ne s’ouvre pas dans l’autre sens. Le même client anonyme, qui lit cette fois :

curl -s -w '\n%{http_code}\n' http://localhost:49900/api/v1/data/crm_lead
curl -s -w '\n%{http_code}\n' http://localhost:49900/api/v1/data/crm_lead/R2Wkp0koxTAw53tN
{"error":"UNAUTHENTICATED","code":"UNAUTHENTICATED","message":"Authentication is required to access this endpoint."}
401

Les deux répondent 401 — la liste, et la ligne que le visiteur a créée une seconde plus tôt. L’auteur de la soumission ne voit jamais l’enregistrement stocké : le 201 porte son id, et l’id n’ouvre rien. « The public path only ever inserts; it never lists » (le chemin public ne fait qu’insérer ; il ne liste jamais).

6. Retirer le formulaire ferme les deux points d’accès. Gardez le publicLink, mettez enabled: false, redémarrez objectstack dev :

GET  /api/v1/forms/contact-us         → 404 {"code":"FORM_NOT_FOUND","error":"No public form configured at /forms/contact-us"}
POST /api/v1/forms/contact-us/submit  → 404 {"code":"FORM_NOT_FOUND","error":"No public form configured at /forms/contact-us"}

Le nombre de leads lu en tant qu’administrateur était le même avant et après ces deux appels : rien n’a été créé. Cinq codes de statut racontent toute l’histoire de la porte : 201, 201, 400, 401, 404.

La question du profil invité, réglée en amont

Sur 17.7.0, la documentation et l’exemple livré se contredisaient sur ce qui autorise l’écriture. Le guide disait, comme il le dit toujours : « Declare a public form view; the submit route derives a narrow authorization from that declaration. No guest profile, no broad grant » (déclarez une vue de formulaire publique ; la route de soumission en dérive une autorisation étroite. Pas de profil guest, pas de droit large). L’exemple CRM livrait un ensemble de permissions nommé guest_portal avec allowCreate: true sur crm_lead, son commentaire disait que ce profil « authorizes the write » (autorise l’écriture), et la docstring de la route avertissait que sans lui le middleware « falls open » (échoue en position ouverte). J’ai retiré l’ensemble du CRM 17.7.0, relancé les sondes, obtenu des réponses identiques et signalé la contradiction (issue objectstack #24130).

Elle a été corrigée en 18.0 (PR #24237). Au tag 18.3.0, le commentaire au-dessus de l’ensemble dit « It does NOT authorize the public Web-to-Lead form (lead.view.ts web_to_lead). That form’s own declaration does » (il n’autorise PAS le formulaire public Web-to-Lead ; c’est la propre déclaration de ce formulaire qui le fait), et se termine par « the submit answers the created id only » (la soumission ne répond que l’id créé). Le commentaire du formulaire lui-même dit « No permission set is needed, GuestPortalProfile included » (aucun ensemble de permissions n’est nécessaire, GuestPortalProfile compris). Le guide a gagné un paragraphe qui commence par « The submit’s execution context also names a guest_portal permission set. That name authorizes nothing » (le contexte d’exécution de la soumission nomme aussi un ensemble de permissions guest_portal. Ce nom n’autorise rien), et la docstring « falls open » a disparu. L’exemple garde l’ensemble parce que l’ADR-0138 l’enregistre comme référence du nom que demande le contexte de soumission.

J’ai tout de même refait la comparaison sur 18.3.0 : l’application avec l’ensemble, puis avec permissions: [], l’artefact compilé ne listant plus aucun ensemble de permissions.

SondeAvec guest_portalSans guest_portal
POST …/submit avec name, email201 { id }, ligne créée201 { id }, ligne créée
POST …/submit glissant status, lead_score, owner_id, organization_id, queue201 { id } ; relu status: "new", lead_score: 0, owner_id: null, queue: null, organization_id du serveuridentique
POST …/submit sans email400 VALIDATION_FAILED, email requiredidentique
GET /api/v1/data/crm_lead anonyme401 UNAUTHENTICATED401 UNAUTHENTICATED
GET anonyme de la ligne créée401 UNAUTHENTICATED401 UNAUTHENTICATED

Identique, ligne pour ligne, comme sur 17.7.0. Ce que le nom atteint encore est étroit : un ensemble enregistré sous guest_portal fournit les permissions de champ que le masqueur de résultats du droit applique à l’enregistrement rendu à la route, un enregistrement que le visiteur ne voit plus. Le conseil pratique tient donc : n’ajoutez pas de profil invité pour faire fonctionner un formulaire public. Il ajoute à votre matrice d’accès un droit permanent que la porte publique ne consulte jamais. Si votre application versionne un instantané de la matrice d’accès, comme le CRM complet, ajouter ou retirer un tel ensemble est un changement que le build vous oblige à relire ; ce contrôle est un sujet en soi.

Un point où l’exemple et le guide divergent encore : l’étape trois du guide est « Set sharingModel: 'private' on the object so submissions are staff-scoped after creation » (définissez sharingModel: 'private' sur l’objet pour que les soumissions soient limitées au personnel après création), et l’objet lead du CRM est délibérément en public_read_write, avec un commentaire expliquant que la démo est partagée dans l’organisation à dessein. Cela n’affaiblit pas la porte — la valeur par défaut de l’organisation gouverne ce que les utilisateurs connectés voient des lignes des autres, et l’appelant anonyme est refusé avant que rien de cela ne soit consulté —, mais c’est un conseil à suivre sur votre propre objet de collecte.

Ce que cette porte ne fait pas

  • Pas de limitation de débit par défaut, pas de captcha, pas d’anti-spam. Sur l’application générée par défaut, 40 soumissions d’un même client en 0,89 seconde ont toutes répondu 201. Le runtime livre bien un limiteur, à l’échelle du serveur et désactivé par défaut : avec server: { security: { rateLimit: { enabled: true, windowMs: 60_000, maxRequests: 10 } } } dans la stack, le même client a reçu neuf 201 puis 429 RATE_LIMIT_EXCEEDED avec un en-tête Retry-After ; son GET précédent du formulaire avait puisé dans le même budget. Il mesure chaque route et, sans session, compte par l’adresse de l’appelant, si bien que derrière un reverse proxy il compte l’adresse du proxy, sauf si vous déclarez server.trustProxy — ce qui n’est sûr que si ce proxy réécrit les en-têtes de transfert. C’est un budget, pas une protection contre les bots : placez la protection de bordure habituelle devant /api/v1/forms/*.
  • Le required du formulaire ne couvre que ce que le formulaire affiche toujours. Une entrée avec hidden: true, ou avec un visibleWhen sur l’entrée ou sa section, n’est pas jugée à la soumission, car rien côté serveur n’évalue le visibleWhen d’un formulaire. Si un tel champ doit être présent, rendez-le obligatoire sur l’objet ou vérifiez-le dans un hook.
  • La réponse n’apprend presque rien au visiteur, à dessein. Le 201 porte le nouvel id et aucun compte rendu de ce qui a été gardé ou écarté. Un formulaire qui doit afficher une référence peut nommer dans son panneau de remerciement un champ généré par le moteur, comme un numéro de dossier autonumber, sous la forme {{record.case_number}} ; selon le guide, la réponse porte alors cette valeur sous fields, et os validate ainsi que le contrôle de publication refusent tout autre champ. Le formulaire du CRM n’en nomme aucun, je n’ai donc pas mesuré cette branche. Sur 17.7.0, l’écho portait aussi ce qu’un hook beforeInsert ou afterInsert avait tamponné, et cet article conseillait de tenir les hooks qui consultent des enregistrements à l’écart des objets de collecte publique ; depuis la 18.0, la route filtre ce qui est montré au visiteur, et ce conseil est retiré.
  • Le propriétaire n’est personne. owner_id reste null sur une soumission publique ; le plugin de sécurité le laisse « for object hooks / the first-admin bootstrap to assign » (pour que les hooks d’objet ou l’amorçage du premier administrateur l’attribuent). Si votre processus attribue des leads, faites-le dans un hook ou un flux — n’attendez pas que la ligne appartienne à une personne.
  • Un retrait demande un redémarrage. Depuis la 18.2, objectstack dev compile la configuration au démarrage par défaut, et un simple redémarrage a fermé le formulaire ici. Sur 17.7.0, ma première tentative n’a rien changé, parce qu’un démarrage --no-watch servait le build précédent. Une boucle pilotée par un agent qui ne lit que des codes de statut devrait vérifier le 404, pas le supposer.
  • Le retrait à travers les couches de métadonnées a des bords que cet article n’a pas parcourus. La copie enregistrée d’une vue par une organisation, et les formulaires livrés par des paquets, suivent leurs propres règles dans le guide. L’interrupteur unique testé ici est celui dont la plupart des applications ont besoin.

Les cinq questions d’un relecteur

Que le formulaire ait été écrit par une personne ou par un agent, la relecture est la même, et elle est courte parce que le droit est le formulaire.

  1. Quels formulaires sont publics ? Toute vue de formulaire avec sharing.enabled, sharing.allowAnonymous et un publicLink définis est un point d’accès anonyme. L’artefact compilé les liste ; il n’y a nulle part ailleurs où regarder.
  2. Que contient sections[].fields ? Cette liste, moins les treize noms gérés par le serveur, est la surface d’écriture publique. Lisez-la comme vous liriez une règle de pare-feu.
  3. L’objet remplit-il ce que le formulaire omet ? Chaque champ obligatoire que le formulaire ne collecte pas a besoin d’une valeur par défaut au niveau de l’objet — status dans l’exemple — sinon la soumission échoue. Un champ que le formulaire affiche et marque required est appliqué à la porte ; un champ qu’il masque ou n’affiche que sous condition a besoin de son required sur l’objet.
  4. Y a-t-il un droit anonyme permanent quelque part ? Un ensemble de permissions lié à une position guest ou everyone, ou un ensemble guest_portal ajouté « pour faire marcher le formulaire », n’est pas nécessaire et n’élargit rien de ce que la porte utilise. Traitez sa présence comme une question.
  5. Qu’y a-t-il devant la porte ? La protection contre les bots est l’affaire du déploiement. Le limiteur propre au runtime est un budget à l’échelle du serveur, et il est désactivé tant que vous ne le déclarez pas.

C’est le même argument sur le lieu d’application que où s’appliquent les permissions des agents, appliqué à un appelant sans aucune identité : la vérification tourne dans la route et la couche de données, pas dans l’interface qui se trouve masquer les champs. Et c’est la réponse au problème de relecture que décrit l’article sur Lovable — le propriétaire responsable peut lire la liste de cinq champs et la valider, ce qui n’est pas vrai d’une politique générée. L’entrée du glossaire sur le modèle de permissions en donne la forme générale ; cette porte est la seule exception délibérée à « refuser par défaut », et elle a la forme du formulaire.

Pointez votre agent dessus

Si un agent écrit votre application, ce qu’il faut lui transmettre n’est pas cet article mais la règle : un formulaire public est une entrée de formViews avec un bloc sharing, la liste des champs est la frontière, le required du formulaire est vérifié à la porte, l’objet possède les valeurs par défaut, et aucun ensemble de permissions n’intervient. Le guide de configuration d’un agent de code pour ObjectStack explique comment cette règle atteint l’agent — le fichier de règles, le point d’accès MCP qu’expose une application en cours d’exécution, et la spec. npm create objectstack@latest installe 18.3.0, la version sur laquelle cette transcription a tourné.

Pour l’exécuter, le dépôt ObjectStack fournit ObjectOS Community — l’image ghcr.io/objectstack-ai/objectstack, os start et la Console, gratuite et auto-hébergée — tandis qu’ObjectOS Cloud est l’édition hébergée et ObjectOS Enterprise l’édition autogérée, sous licence.