Trigger: Felder mit festem Typ, streng als Standard, nachsichtig abschaltbar (#725)

Das Token sagt, wer schickt - nicht, wer den Inhalt geschrieben hat. Felder
mit festem Typ (Kennung, Zahl, Ganzzahl, Datum, Datum+Uhrzeit, ja/nein,
Auswahl, Mailadresse) koennen keine Anweisung tragen und gehen als geprueft
an den Agenten, auch als {feld} im Auftrag. Freitext bleibt Fremdinhalt.

Streng (Standard): unpassende Lieferung -> 400 mit Grund, im Audit.
Nachsichtig (fuer Absender, die sich nicht an die Felder halten): nichts
wird abgewiesen, Passendes gilt als geprueft, der Rest bleibt Fremdinhalt;
der Trigger ist dann 'unsicher' und wird so angezeigt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-02 10:56:00 +02:00
co-authored by Claude Opus 5.5
parent 38bd304ca6
commit ace215eaa2
10 changed files with 562 additions and 28 deletions
@@ -1057,6 +1057,25 @@ button.klein {
overflow-wrap: anywhere;
}
.trigger-felder {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 6px;
}
.trigger-feld {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.trigger-feld input[type="text"] {
flex: 1 1 140px;
min-width: 0;
}
.trigger-auftrag {
margin: 6px 0;
padding: 6px 10px;
@@ -1152,7 +1152,7 @@
"trigger.exampleHint": "In n8n: ein HTTP-Request-Knoten, Methode POST, diese Adresse, Kopfzeile Authorization: Bearer und das Token. Ein JSON-Rumpf mit \"title\" oder \"subject\" benennt die Aufgabe.",
"trigger.name": "Name",
"trigger.instruction": "Auftrag an den Agenten",
"trigger.instructionHint": "Was der Agent mit jeder Lieferung tut — z. B. „Prüfe die Bestellung gegen den Lagerbestand, trage sie unter bestellungen/ ein und melde dem Kundendienst, wenn etwas fehlt.“ Dieser Text kommt von dir und gilt als vertrauenswürdig; die gelieferten Daten nicht.",
"trigger.instructionHint": "Was der Agent mit jeder Lieferung tut — z. B. „Prüfe Bestellung {order} gegen den Lagerbestand und trage sie unter bestellungen/ ein.“ {feld} setzt einen geprüften Feldwert ein. Dieser Text kommt von dir und gilt als vertrauenswürdig; die gelieferten Daten nicht.",
"trigger.instructionPlaceholder": "Was soll der Agent mit jedem Ereignis tun?",
"trigger.noInstruction": "Kein Auftrag — der Agent erfährt nur, dass ein Ereignis kam, und muss raten, was zu tun ist.",
"trigger.slug": "Adressname",
@@ -1172,6 +1172,27 @@
"trigger.newToken": "Neues Token",
"trigger.newTokenConfirm": "Neues Token für „{name}“ ausgeben? Das alte gilt ab sofort nicht mehr.",
"trigger.newTokenTitle": "Neues Token für {name}",
"trigger.fields": "Felder einer Lieferung",
"trigger.fieldsHint": "Die Werte, die der Absender mitgibt, jeweils mit festem Typ. Ein Wert, der zu seinem Typ passt, kann keine Anweisung enthalten und geht als geprüft an den Agenten; Freitext bleibt Fremdinhalt. Ohne Felder ist alles Fremdinhalt.",
"trigger.fieldsSummary": "Felder: {fields}",
"trigger.fieldName": "Feldname, z. B. order",
"trigger.fieldType": "Typ",
"trigger.fieldValues": "erlaubte Werte, mit Komma getrennt",
"trigger.required": "Pflicht",
"trigger.addField": "+ Feld",
"trigger.removeField": "Feld entfernen",
"trigger.type.id": "Kennung",
"trigger.type.number": "Zahl",
"trigger.type.integer": "Ganzzahl",
"trigger.type.date": "Datum",
"trigger.type.datetime": "Datum und Uhrzeit",
"trigger.type.boolean": "ja/nein",
"trigger.type.enum": "Auswahl",
"trigger.type.email": "Mailadresse",
"trigger.strict": "Lieferungen streng prüfen (empfohlen)",
"trigger.strictHint": "An: Eine Lieferung, deren Felder nicht passen, wird abgewiesen (400), und der Absender erfährt warum. Aus: für Absender, die sich nicht an die Felder halten — nichts wird abgewiesen, passende Werte gelten weiter als geprüft, der Rest bleibt Fremdinhalt. Ein solcher Auslöser ist unsicher.",
"trigger.unsafe": "unsicher",
"trigger.lenientNote": "Nachsichtig: Lieferungen mit unpassenden Feldern werden nicht abgewiesen — unpassende Werte erreichen den Agenten nur als Fremdinhalt.",
"runtime.title": "Laufzeit",
"runtime.hint": "Native Läufe nutzen unsere Engine mit OpenRouter. Eine fremde Laufzeit arbeitet in der Sandbox auf einer Kopie des Arbeitsbereichs: Was sie ändert, kommt als Vorschlag zurück und wird nie von selbst übernommen. Taskboard und Kollegen erreicht sie nur über den MCP-Server dieser Instanz.",
@@ -1152,7 +1152,7 @@
"trigger.exampleHint": "In n8n: an HTTP Request node, method POST, this address, header Authorization: Bearer and the token. A JSON body with \"title\" or \"subject\" names the task.",
"trigger.name": "Name",
"trigger.instruction": "Instruction for the agent",
"trigger.instructionHint": "What the agent does with every delivery — e.g. \"Check the order against stock, book it in orders/ and tell the customer desk if something is missing.\" This text comes from you and is trusted; the delivered data is not.",
"trigger.instructionHint": "What the agent does with every delivery — e.g. \"Check order {order} against stock and book it in orders/.\" {field} puts in a checked field value. This text comes from you and is trusted; the delivered data is not.",
"trigger.instructionPlaceholder": "What should the agent do with each event?",
"trigger.noInstruction": "No instruction — the agent only learns that an event came in and must guess what to do.",
"trigger.slug": "Address name",
@@ -1172,6 +1172,27 @@
"trigger.newToken": "New token",
"trigger.newTokenConfirm": "Issue a new token for '{name}'? The old one stops working at once.",
"trigger.newTokenTitle": "New token for {name}",
"trigger.fields": "Fields of a delivery",
"trigger.fieldsHint": "The values the sender passes on, each with a strict type. A value that fits its type cannot carry an instruction and goes to the agent as checked; free text stays foreign content. No fields: everything is foreign content.",
"trigger.fieldsSummary": "Fields: {fields}",
"trigger.fieldName": "field name, e.g. order",
"trigger.fieldType": "Type",
"trigger.fieldValues": "allowed values, comma-separated",
"trigger.required": "required",
"trigger.addField": "+ Field",
"trigger.removeField": "Remove field",
"trigger.type.id": "identifier",
"trigger.type.number": "number",
"trigger.type.integer": "whole number",
"trigger.type.date": "date",
"trigger.type.datetime": "date and time",
"trigger.type.boolean": "yes/no",
"trigger.type.enum": "choice",
"trigger.type.email": "mail address",
"trigger.strict": "Check deliveries strictly (recommended)",
"trigger.strictHint": "On: a delivery whose fields do not fit is refused (400) and the sender learns why. Off: for senders that do not keep to the fields — nothing is refused, values that fit are still checked, the rest stays foreign content. Such a trigger is unsafe.",
"trigger.unsafe": "unsafe",
"trigger.lenientNote": "Lenient: deliveries are not refused when their fields do not fit — values that do not fit reach the agent only as foreign content.",
"runtime.title": "Runtime",
"runtime.hint": "Native runs use our engine with OpenRouter. A foreign runtime works in the sandbox on a copy of the workspace: what it changes comes back as a proposal and is never applied by itself. It reaches the taskboard and colleagues only through this instance's MCP server.",
@@ -10,6 +10,7 @@ import {
erstelleElement,
erstelleKarte,
erstelleModal,
erstellePlakette,
formatiereDatumZeit,
zeigeMeldung
} from "../modules/dom.js";
@@ -233,11 +234,22 @@ export function erstelleTriggerKarte(agent) {
? erstelleElement("div", { klasse: "trigger-auftrag", text: trigger.instruction })
: erstelleElement("div", { klasse: "werkzeug-beschreibung text-klein", text: `⚠ ${t("trigger.noInstruction")}` });
const felder = (trigger.fields ?? []).length > 0
? erstelleElement("div", { klasse: "werkzeug-beschreibung text-klein", text: t("trigger.fieldsSummary", { fields: feldListe(trigger.fields) }) })
: null;
const titel = erstelleElement("strong", { text: trigger.label || trigger.slug });
const kopf = trigger.strict
? titel
: erstelleElement("span", {}, titel, " ", erstellePlakette(t("trigger.unsafe"), "warnung"));
return erstelleElement("div", { klasse: `werkzeug-zeile${trigger.active ? "" : " mcp-token-inaktiv"}` },
erstelleElement("div", { klasse: "werkzeug-info" },
erstelleElement("strong", { text: trigger.label || trigger.slug }),
kopf,
erstelleElement("div", { klasse: "trigger-adresse", text: triggerAdresse(trigger.slug) }),
auftrag,
...(felder ? [felder] : []),
...(trigger.strict ? [] : [erstelleElement("div", { klasse: "werkzeug-beschreibung text-klein", text: `⚠ ${t("trigger.lenientNote")}` })]),
erstelleElement("div", { klasse: "werkzeug-beschreibung gedaempft", text: status })),
aktionen);
}
@@ -257,7 +269,61 @@ function triggerAdresse(slug) {
return `${location.origin}/hooks/${instanz ? `${encodeURIComponent(instanz)}/` : ""}trigger/${encodeURIComponent(slug)}`;
}
/** Create or edit a trigger: name, address name, the instruction for the agent, priority, rate. */
const FELDTYPEN = ["id", "number", "integer", "date", "datetime", "boolean", "enum", "email"];
/** "order (identifier, required), amount (number)" — for the list. */
function feldListe(felder) {
return felder.map((f) => {
const art = f.type === "enum" ? `${t("trigger.type.enum")}: ${(f.values ?? []).join(" | ")}` : t(`trigger.type.${f.type}`);
return `${f.name} (${art}${f.required ? `, ${t("trigger.required")}` : ""})`;
}).join(", ");
}
/** A sample body from the fields — what the sender should send. */
function beispielRumpf(felder) {
if (!felder || felder.length === 0) return { title: "New order", order: "4711" };
const muster = { id: "A-4711", number: 129.9, integer: 3, date: "2026-10-02", datetime: "2026-10-02T09:15:00+02:00", boolean: true, email: "kunde@example.org" };
const rumpf = { title: "New event" };
for (const f of felder) rumpf[f.name] = f.type === "enum" ? (f.values ?? [])[0] ?? "" : muster[f.type];
return rumpf;
}
/** The field editor: one row per field — name, type, required, allowed values for a choice. */
function erstelleFeldEditor(felder) {
const zeilen = erstelleElement("div", { klasse: "trigger-felder" });
function zeile(f = { name: "", type: "id", required: false, values: [] }) {
const name = erstelleElement("input", { type: "text", maxlength: "40", placeholder: t("trigger.fieldName"), value: f.name });
const typ = erstelleElement("select", { "aria-label": t("trigger.fieldType") },
...FELDTYPEN.map((x) => erstelleElement("option", { value: x, text: t(`trigger.type.${x}`) })));
typ.value = f.type;
const werte = erstelleElement("input", { type: "text", placeholder: t("trigger.fieldValues"), value: (f.values ?? []).join(", ") });
const pflicht = erstelleElement("input", { type: "checkbox" });
pflicht.checked = !!f.required;
const zeigeWerte = () => { werte.hidden = typ.value !== "enum"; };
typ.addEventListener("change", zeigeWerte);
zeigeWerte();
const el = erstelleElement("div", { klasse: "trigger-feld" },
name, typ, werte,
erstelleElement("label", { klasse: "gruppe-horizontal" }, pflicht, erstelleElement("span", { text: t("trigger.required") })),
erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: "✕", "aria-label": t("trigger.removeField"), onclick: () => el.remove() }));
el.lesen = () => ({
name: name.value.trim(),
type: typ.value,
required: pflicht.checked,
values: typ.value === "enum" ? werte.value.split(",").map((v) => v.trim()).filter(Boolean) : null
});
zeilen.append(el);
}
for (const f of felder ?? []) zeile(f);
const hinzu = erstelleElement("button", { type: "button", klasse: "schaltflaeche flach klein", text: t("trigger.addField"), onclick: () => zeile() });
const block = erstelleElement("div", {}, zeilen, hinzu);
block.lesen = () => [...zeilen.children].map((z) => z.lesen()).filter((f) => f.name);
return block;
}
/** Create or edit a trigger: name, address name, the instruction for the agent, fields, priority, rate. */
function zeigeTriggerFormular(agent, trigger, speichern) {
const name = erstelleElement("input", { type: "text", maxlength: "60", placeholder: t("trigger.labelPlaceholder"), value: trigger?.label ?? "" });
const slug = erstelleElement("input", { type: "text", maxlength: "40", placeholder: t("trigger.slugPlaceholder"), value: trigger?.slug ?? "" });
@@ -267,12 +333,20 @@ function zeigeTriggerFormular(agent, trigger, speichern) {
...[5, 4, 3, 2, 1].map((p) => erstelleElement("option", { value: String(p), text: t(`trigger.priority${p}`) })));
prioritaet.value = String(trigger?.priority ?? 3);
const proStunde = erstelleElement("input", { type: "number", min: "1", max: "600", step: "1", value: String(trigger?.perHour ?? 30) });
const felder = erstelleFeldEditor(trigger?.fields ?? []);
const streng = erstelleElement("input", { type: "checkbox" });
streng.checked = trigger ? trigger.strict !== false : true;
const formular = erstelleElement("div", { klasse: "formular-raster" },
erstelleElement("label", { text: t("trigger.name") }), name,
erstelleElement("label", { text: t("trigger.instruction") }),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.instructionHint") }),
auftrag,
erstelleElement("label", { text: t("trigger.fields") }),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.fieldsHint") }),
felder,
erstelleElement("label", { klasse: "gruppe-horizontal" }, streng, erstelleElement("span", { text: t("trigger.strict") })),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.strictHint") }),
erstelleElement("label", { text: t("trigger.slug") }),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t(trigger ? "trigger.slugEditHint" : "trigger.slugHint") }),
slug,
@@ -293,7 +367,9 @@ function zeigeTriggerFormular(agent, trigger, speichern) {
slug: slug.value.trim() || null,
instruction: auftrag.value.trim(),
priority: Number(prioritaet.value),
perHour: Number(proStunde.value)
perHour: Number(proStunde.value),
fields: felder.lesen(),
strict: streng.checked
});
modal.schliessen();
} catch (err) {
@@ -312,7 +388,7 @@ function zeigeTriggerFormular(agent, trigger, speichern) {
/** The trigger's token and address once — with how n8n (or anything else) calls it. */
function zeigeNeuenAusloeser(neu, titel) {
const adresse = triggerAdresse(neu.info.slug);
const beispiel = `curl -X POST "${adresse}" \\\n -H "Authorization: Bearer ${neu.token}" \\\n -H "Content-Type: application/json" \\\n -d '{"title":"New order","order":"4711"}'`;
const beispiel = `curl -X POST "${adresse}" \\\n -H "Authorization: Bearer ${neu.token}" \\\n -H "Content-Type: application/json" \\\n -d '${JSON.stringify(beispielRumpf(neu.info.fields))}'`;
const feld = (wert, zeilen) => {
const f = erstelleElement("textarea", { readonly: true, rows: String(zeilen), klasse: "mcp-token-feld" });