Eingehende Ausloeser: n8n und andere starten Agenten (#725, Stufe 1)

Something outside hands an agent a task - an n8n workflow on a mail, a
schedule, a form, or any system that sends an HTTP request.

- Core/Triggers/InboundTriggers: one token per trigger and agent, kept as a
  hash and shown once; a rate per hour; deliveries up to 64 KB. Each
  delivery becomes a task whose content is framed as foreign content with its
  origin; every delivery, accepted or refused, is in the audit (source
  "trigger").
- POST /hooks/trigger on the instance (MCP socket, own token, no browsers);
  the manager forwards /hooks/{instance}/trigger. /api/triggers to issue,
  list and revoke.
- Agent view: a triggers card with the address and an n8n/curl example.
- Nothing writes back: no right to create or change workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 13:19:05 +02:00
co-authored by Claude Opus 5.5
parent 47a20913b0
commit 73565eff18
14 changed files with 607 additions and 5 deletions
@@ -153,3 +153,123 @@ function zeigeNeuesToken(neu) {
const modal = erstelleModal(t("mcp.issuedTitle", { agent: neu.info.agentId }), inhalt, [fertig]);
modal.oeffnen();
}
/**
* #725: Inbound triggers — n8n or any system that sends a request hands this agent a task.
* One token per trigger, shown once; each delivery becomes a task whose content is framed as
* foreign content. Nothing goes back: ClawdDotNet gets no right to change workflows.
*/
export function erstelleTriggerKarte(agent) {
const liste = erstelleElement("div", { klasse: "mcp-token-liste" });
const bezeichnung = erstelleElement("input", { type: "text", placeholder: t("trigger.labelPlaceholder"), maxlength: "60" });
const proStunde = erstelleElement("input", { type: "number", min: "1", max: "600", step: "1", value: "30", "aria-label": t("trigger.perHour") });
const ausgeben = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer",
text: t("trigger.issue"),
onclick: async () => {
ausgeben.disabled = true;
try {
const neu = await api("/triggers", {
method: "POST",
body: { agentId: agent.agentId, label: bezeichnung.value.trim(), perHour: Number(proStunde.value) }
});
bezeichnung.value = "";
zeigeNeuenAusloeser(neu);
await lade();
} catch (err) {
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
} finally {
ausgeben.disabled = false;
}
}
});
async function lade() {
try {
const alle = (await api("/triggers")) || [];
const eigene = alle.filter((x) => x.agentId.toLowerCase() === agent.agentId.toLowerCase());
liste.replaceChildren(...(eigene.length === 0
? [erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.none") })]
: eigene.map(zeile)));
} catch (err) {
liste.replaceChildren(erstelleElement("p", { klasse: "gedaempft text-klein", text: err.message }));
}
}
function zeile(trigger) {
const status = trigger.active
? t("trigger.status", {
perHour: trigger.perHour,
received: trigger.received,
last: trigger.lastUsedAt ? formatiereDatumZeit(trigger.lastUsedAt) : "—"
})
: t("mcp.revoked", { time: formatiereDatumZeit(trigger.revokedAt) });
const aktionen = erstelleElement("div", { klasse: "werkzeug-aktionen" });
if (trigger.active) {
aktionen.append(erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach klein",
text: t("mcp.revoke"),
onclick: async () => {
try {
await api(`/triggers/${encodeURIComponent(trigger.id)}`, { method: "DELETE" });
zeigeMeldung(t("mcp.revokedMessage"), "info");
await lade();
} catch (err) {
zeigeMeldung(t("mcp.failed", { error: err.message }), "gefahr");
}
}
}));
}
return erstelleElement("div", { klasse: `werkzeug-zeile${trigger.active ? "" : " mcp-token-inaktiv"}` },
erstelleElement("div", { klasse: "werkzeug-info" },
erstelleElement("strong", { text: trigger.label || trigger.id }),
erstelleElement("div", { klasse: "werkzeug-beschreibung gedaempft", text: status })),
aktionen);
}
lade();
return erstelleKarte(t("trigger.title"), [
erstelleElement("p", { klasse: "gedaempft", text: t("trigger.hint") }),
liste,
erstelleElement("div", { klasse: "mcp-token-formular" }, bezeichnung, proStunde, ausgeben)
]);
}
/** The trigger's token and address once — with how n8n (or anything else) calls it. */
function zeigeNeuenAusloeser(neu) {
const instanz = holeAktiveInstanz();
const adresse = `${location.origin}/hooks/${instanz ? `${encodeURIComponent(instanz)}/` : ""}trigger`;
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 feld = (wert, zeilen) => {
const f = erstelleElement("textarea", { readonly: true, rows: String(zeilen), klasse: "mcp-token-feld" });
f.value = wert;
f.addEventListener("focus", () => f.select());
return f;
};
const inhalt = erstelleElement("div", { klasse: "mcp-token-neu" },
erstelleElement("p", { klasse: "text-klein", text: `⚠ ${t("mcp.shownOnce")}` }),
erstelleElement("label", { text: t("trigger.address") }),
feld(adresse, 1),
erstelleElement("label", { text: t("mcp.token") }),
feld(neu.token, 2),
erstelleElement("label", { text: t("trigger.example") }),
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("trigger.exampleHint") }),
feld(beispiel, 5));
const fertig = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer",
text: t("common.close"),
onclick: () => modal.schliessen()
});
const modal = erstelleModal(t("trigger.issuedTitle", { agent: neu.info.agentId }), inhalt, [fertig]);
modal.oeffnen();
}