@pierre-gilles, ich habe ein separates Thema erstellt, um deinen Post zu externen Integrationen nicht zu sehr zu überladen.
Ich habe alles an Claude weitergegeben und hier ist seine Analyse zu Free Mobile:
1. Kontext
Gladys erhält externe Integrationen: Programme, die in isolierten Docker-Containern laufen, von Gladys überwacht werden
und mit ihm über eine Host-API (REST) + ein ausgehendes WebSocket kommunizieren, eingewickelt in
@gladysassistant/integration-sdk. Zwei types von Manifesten existieren heute (manifest.schema.json, Zeile 16-19):
device— stellt Geräte über die Bildschirme Geräte / Entdeckung / Konfiguration bereit.communication— Messaging-Kanäle (Bots wie Telegram): keine Geräte-Bildschirme, der Benutzer verbindet sein
Konto über die Gladys-Benutzeroberfläche, und die Integration tauscht Nachrichten über die Host-API aus.
Ich bringe den historischen Dienst Free Mobile SMS, bisher in den Kern von Gladys integriert, zu einer externen Integration. Free Mobile ist ein Kommunikationskanal, daher ist type: "communication" die natürliche Wahl — aber dies hat ein strukturelles Loch im aktuellen Kommunikationsmodell aufgedeckt. Dieses Dokument erklärt dieses Loch, beweist es anhand des Codes und schlägt einen minimalen und rückwärtskompatiblen Fix vor.
2. Wie funktioniert das Kommunikationsmodell heute
Eine Kommunikationsintegration spricht mit Gladys nur über verknüpfte Kontakte. Gladys sagt nie „sende an
William“; es sagt „sende an den Kontakt X“, wobei X eine Kennung ist, die die Integration selbst während der Verknüpfung erstellt hat. Drei Bausteine (README integration-sdk + Host-API-Controller):
- Verknüpfung (Zustimmung) — der Benutzer klickt „Mein Konto verknüpfen“ in der Gladys-Benutzeroberfläche, was einen kurzen Code
generiert (einmalige Nutzung, TTL 15 min). Der Benutzer sendet diesen Code an den Bot im externen Kanal; die Integration empfängt ihn und ruftlinkContact(code, contactId)→POST /api/integration/v1/contact/linkauf. - Eingehend —
publishMessage(contactId, text)→POST /api/integration/v1/message. Ein unbekannter (nicht verknüpfter) Kontakt löst einen 404 aus. - Ausgehend —
onSendMessage(contactId, message): Antworten des Gehirns und Benachrichtigungen, die von der Integration in den externen Kanal geliefert werden.
Der Code existiert, um einen eingehenden Weg zu beweisen
Der ganze Sinn des Codes ist, dass er über den externen Kanal läuft: indem er ihn an den Bot zurücksendet, beweist die Person
in Telegram/Signal, dass sie der Gladys-Benutzer ist, der ihn generiert hat. Dies wird durch den Code selbst bestätigt:
externalIntegration.createLinkCode.js— Kommentar: « The user then sends it to the bot in the external
channel, and the integration calls POST /contact/link with it. » Alphabet gewählt, um « typed in a chat » zu sein.externalIntegration.linkContact.js— der Code muss im Cache (erstellt von der Benutzeroberfläche) vorhanden sein und nicht abgelaufen sein,
sonstNotFoundError('INVALID_LINK_CODE'). Es gibt keine andere Möglichkeit, einen Kontakt zu erstellen. Kein Auto-Verknüpfungsweg existiert.
Das Modell unterstellt also einen bidirektionalen Kanal.
3. Das Loch: nur ausgehende Kanäle
Eine ganze Klasse von Kommunikationsintegrationen hat keinen eingehenden Kanal — sie können nur eine
Benachrichtigung pushed:
- Free Mobile SMS (SMS an die eigene Nummer über einen Webhook),
- Pushover, ntfy, Gotify,
- eingehende Discord / Slack Webhooks,
- E-Mail SMTP.
Für alle gilt gleichzeitig:
- Der Code kann nicht transitieren. Es gibt keinen Ort, an den man ihn senden kann — kein Bot, kein eingehender Endpunkt. Der zentrale Schritt des Verknüpfungsflusses des Kerns ist physisch unmöglich.
- Es gibt keine Identität, die durch Hin- und Herbewegungen bewiesen werden muss. Der Benutzer gibt seine eigene Zieladresse — seinen API-Schlüssel, seine Webhook-URL, seine E-Mail-Adresse — in seiner eigenen Gladys-Konfigurationsseite ein, während er bereits in Gladys authentifiziert ist. Die Zustimmung ist die Handlung, dieses Feld auszufüllen und zu speichern. Ein Code, der „das sind Sie im externen Kanal“ beweist, beantwortet eine Frage, die niemand stellt.
Was der Benutzer heute tatsächlich erlebt (Free Mobile)
Die Konfigurationsseite communication (PR #2665, config-page/ConfigTab.jsx Zeile 95) zeigt LinkAccountCard ohne Bedingung für jede Kommunikationsintegration. Der Free Mobile-Benutzer sieht also:
- Die native Karte „Mein Konto verknüpfen“ mit einem Button „Code generieren“ → zeigt z. B.
ABCD2345an und
(sein i18n-Text) bittet darum, „diesen Code an den Bot im externen Kanal zu senden“ — faktisch falsch: Free Mobile hat keinen solchen Kanal. - Die einzige Umgehung, die mit dem aktuellen Kern kompatibel ist, ist eine Manifest-Aktion („Mein Konto verknüpfen“) mit einem Code-Feld: Der Benutzer kopiert
ABCD2345von Karte Nr. 1 und fügt ihn in die Aktion einige Zentimeter weiter unten ein, waslinkContact(code, username)aufruft.
Es ist eine leere Zeremonie: Der Benutzer kopiert einen Code von einer Karte und fügt ihn in eine andere ein, um einen Weg zu beweisen, der nicht existiert. Es ist verwirrend und sieht kaputt aus. Das ist das Problem, das behoben werden muss.
4. Vorschlag: ein optionales Manifest-Feld messaging_mode
Ein optionales Manifest-Feld der obersten Ebene hinzufügen, das nur Sinn ergibt, wenn type: "communication":
{
"type": "communication",
"messaging_mode": "outbound", // "bidirectional" (Standard) | "outbound"
}
"bidirectional"(Standard) — das aktuelle Verhalten, unverändert. Telegram-/Signal-/Matrix-Bots
melden nichts und funktionieren weiterhin genau wie zuvor. Vollständig abwärtskompatibel."outbound"— die Integration liefert nur Nachrichten, sie empfängt nie welche. Der Kern ist dann:- versteckt die native
LinkAccountCard— der Code-Fluss hat hier keinen Sinn; - stellt ein API-Endpoint des Hosts bereit, um den Besitzer der Konfiguration ohne Code zu verknüpfen
(POST /api/integration/v1/contact/self-link { contact_id }): Es verknüpft den authentifizierten
Benutzer der Konfiguration mitcontact_id. Zustimmung = der Benutzer hat seine eigene
Zieladresse auf seiner eigenen Konfigurationsseite ausgefüllt und gespeichert; - alles andere downstream bleibt unverändert —
onSendMessage(contactId, message)wird normalerweise
ausgelöst;publishMessage/ der eingehende Routing findet einfach nie statt.
- versteckt die native
Warum das die richtige Aufteilung ist
- Minimal & abwärtskompatibel. Ein optionales Enum-Feld. Der bidirektionale Vertrag (B.15) ist intakt; die
Code-Flow-Dateien (createLinkCode.js,linkContact.js) werden nicht geändert — der ausgehende ist ein paralleler
Pfad, keine Neuimplementierung. - Löst die ganze Familie, nicht nur Free Mobile (Pushover, ntfy, Webhooks, SMTP…).
- Ehrliche UX. Keine irreführende Anweisung „senden Sie diesen Code an den Bot“, kein Kopiervorgang. Die UX von
Free Mobile reduziert sich auf Identifikator + API-Schlüssel ausfüllen → speichern → fertig, also das
historische Verhalten, das im Kern integriert ist. - Die Sicherheit wird nicht geschwächt.
self-linkverknüpft immer nur den bereits authentifizierten Benutzer der
Konfigurationsseite mit einer Zieladresse, die er selbst eingegeben hat. Keine Benutzer-zu-Benutzer-Verknüpfung,
kein Privilegieneskalation. Es ist genau die Zustimmung, die der Code-Flow bietet (« dieser Benutzer Gladys ist
einverstanden »), minus der Hin- und Rückweg, der nur für einen bidirektionalen Kanal Sinn macht. (Eine
ausgehende Integration kann auch nicht einen anderen Benutzer usurpieren:self-linkist auf den
Besitzer der Konfigurationreqbegrenzt, und es gibt keinen eingehenden Pfad, um Antworten mit der
Autorität von jemand anderem zu empfangen.)
5. Konkrete Änderungen (Ankerpunkte im PR #2665)
| # | Schicht | Datei (PR #2665) | Änderung |
|---|---|---|---|
| 1 | Manifest-Schema | server/lib/external-integration/manifest.schema.json |
optionales Enum messaging_mode hinzufügen ["bidirectional","outbound"], Standard "bidirectional". Die gleiche Datei wird vom Store-Indexer verwendet, daher validieren outbound-Manifests überall. |
| 2 | Kern — Link-Bibliothek | server/lib/external-integration/externalIntegration.selfLinkContact.js (neu) |
Verknüpfe den Besitzer der Konfiguration mit contact_id ohne Code; wiederverwende die bestehende CONTACT_VARIABLE-Speicherung (gleiche Form wie linkContact, ohne Code-Lookup). |
| 3 | Kern — Host-API | server/api/controllers/integrationHost.controller.js + server/api/routes.js |
neue Route post /api/integration/v1/contact/self-link + Controller-Methode, neben dem bestehenden contact/link (routes.js gruppiert die Routen integration/v1/* zusammen). |
| 4 | Kern — UI | front/.../config-page/ConfigTab.jsx (l.95) |
LinkAccountCard nur anzeigen, wenn messaging_mode !== 'outbound' (aus integration.manifest gelesen). |
| 5 | SDK | @gladysassistant/integration-sdk (lib/gladys-integration.js, index.d.ts, README) |
selfLinkContact(contactId) (POST /contact/self-link) neben linkContact hinzufügen; messaging_mode dokumentieren. |
Nichts anderes ändert sich. onSendMessage, getContacts, unlinkContact, die Zustandsmaschine des Containers, das
WebSocket-Protokoll — alles intakt.
Entwurf der neuen Kernbibliothek (basierend auf linkContact.js)
// externalIntegration.selfLinkContact.js
const { CONTACT_VARIABLE } = require('./constants');
const { BadParameters } = require('../../utils/coreErrors');
/**
* @description Verknüpfe den Konfigurationsbesitzer-Benutzer mit einem externen Kontakt OHNE Code
* (nur ausgehende Kommunikationsintegrationen): Die Zustimmung ist der Benutzer, der seine
* eigene Zieladresse auf seiner eigenen Konfigurationsseite ausfüllt. Gespeichert wie linkContact.
* @param {object} service - Der externe Integrationsdienst.
* @param {string} userId - Id des authentifizierten Konfigurationsbesitzer-Benutzers.
* @param {object} body - { contact_id, contact_name? }.
* @returns {Promise<object>} { user: { selector, first_name, language } }.
*/
async function selfLinkContact(
service,
userId,
{ contact_id: contactId, contact_name: contactName } = {},
) {
if (typeof contactId !== 'string' || contactId.length === 0) {
throw new BadParameters('contact_id: muss eine nicht-leere Zeichenkette sein');
}
// (geschützt durch die Host-API: service.manifest.messaging_mode muss 'outbound' sein)
await this.variable.setValue(
CONTACT_VARIABLE,
JSON.stringify({
contact_id: contactId,
contact_name: contactName || null,
linked_at: new Date().toISOString(),
}),
service.id,
userId,
);
// gebe den Benutzer zurück, wie linkContact
}
6. Offene Fragen für den Maintainer
- Form des Endpoints —
POST /contact/self-link { contact_id }geschützt durchmessaging_mode === 'outbound', vs.
automatische Verknüpfung bei der ersten Konfiguration (kein expliziter SDK-Aufruf). Der explizite Aufruf ist
vorhersehbarer und lässt die Integration dencontact_idwählen; das implizite erfordert noch weniger Code
von der Integration. Vorzug? - Benennung —
messaging_mode: "outbound"vs. ein Booleaninbound: falsevs. eine Liste von Fähigkeiten.
messaging_modeliest sich am besten, wenn ein zukünftiger dritter Modus (nur eingehend?) eines Tages erscheint.


