Files
ClawdDotNet/src/ClawdDotNet.Manager.Web/wwwroot/views/agents.js
T
RichardandClaude Opus 5.5 3857a7cab2 Rueckmeldung nach Vertrauensstufe: drei Wege nach oben (#715)
- IAgentTool.ReadsForeignContent: Werkzeuge mit Aussenkontakt erklaeren sich selbst
  (WebFetch, WebMonitor, DirectAPI, Mail, RocketChat, Telegram, TelegramClient,
  SocialMediaManager, FTP, Database); AgentConfig.TreatAsForeignReader kann nur verschaerfen
- ReportingLine: Stufe aus den Werkzeugen, "wer steht darueber", Pruefung des Wegs nach oben
  (nie Aenderungen an Aufgaben darueber, nur eine Ebene hoch, Fremdinhalt-Stufe gar kein
  Freitext) und Rahmung als untrusted_content
- Engine: Nachricht und Spawn nach oben geprueft, Text der Fremdinhalt-Stufe kommt gerahmt an
- Taskboard: task_create/update/comment, reopen und die Dubletten-Notiz folgen dem Berichtsweg
- Dispatcher: Aufgaben von Agenten der Fremdinhalt-Stufe gehen gerahmt in den Lauf
- TaskReports: Erledigt-Signal und Systemmeldung (in Pruefung, blockiert, abgebrochen) an den
  Auftraggeber, je Zustand einmal, gesammelt in einer offenen Aufgabe je Empfaenger
- OrgBriefing nennt den Berichtsweg; WebUI zeigt Stufe und Schalter am Agenten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:16:29 +02:00

607 lines
20 KiB
JavaScript

// ClawdDotNet — Agenten-Verwaltung
//
// Übersicht, Anlegen, Löschen von Agenten, Werkzeug-Schalter und maskierte Einstellungen.
import { api } from "../modules/api.js";
import { abonnieren } from "../modules/events.js";
import { t } from "../modules/i18n.js";
import {
erstelleElement,
erstelleIcon,
PFADE,
erstellePlakette,
erstelleKarte,
erstelleModal,
zeigeMeldung
} from "../modules/dom.js";
let gewaehlterAgentId = null;
let agenten = [];
let entladeEreignisse = [];
export async function rendereAgenten(parameter = {}) {
const container = document.getElementById("ansicht-inhalt");
if (!container) return;
container.replaceChildren();
entladeEreignisse.forEach((c) => c());
entladeEreignisse = [];
const layout = erstelleElement("div", { klasse: "agenten-ansicht" });
const kopfZeile = erstelleElement("div", { klasse: "ansicht-kopf" });
kopfZeile.append(
erstelleElement("h1", { text: t("agents.title") }),
erstelleElement(
"button",
{
klasse: "schaltflaeche primaer",
onclick: oeffneNeuerAgentDialog
},
erstelleIcon(PFADE.plus, 16),
erstelleElement("span", { text: ` ${t("agents.newAgent")}` })
)
);
const raster = erstelleElement("div", { klasse: "agenten-raster" });
const listeContainer = erstelleElement("div", { klasse: "agenten-liste-bereich" });
const detailContainer = erstelleElement("div", { klasse: "agenten-detail-bereich" });
raster.append(listeContainer, detailContainer);
layout.append(kopfZeile, raster);
container.append(layout);
await ladeUndRendereListe(parameter.agentId);
entladeEreignisse.push(
abonnieren("instance", (daten) => {
if (daten.kind === "Agents") {
ladeUndRendereListe(gewaehlterAgentId);
}
})
);
}
async function ladeUndRendereListe(zielId = null) {
const listeContainer = document.querySelector(".agenten-liste-bereich");
if (!listeContainer) return;
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("agents.loading") })
);
try {
agenten = await api("/agents");
listeContainer.replaceChildren();
if (!agenten || agenten.length === 0) {
const leerKasten = erstelleElement("div", { klasse: "hinweis-zentriert" });
leerKasten.append(
erstelleElement("p", { klasse: "gedaempft", text: t("agents.empty") }),
erstelleElement("button", {
klasse: "schaltflaeche primaer klein",
onclick: oeffneNeuerAgentDialog,
style: "margin-top: 10px;"
}, erstelleIcon(PFADE.plus, 14), erstelleElement("span", { text: ` ${t("agents.createFirst")}` }))
);
listeContainer.append(leerKasten);
rendereDetails(null);
return;
}
const auswahlId = zielId && agenten.some((a) => a.agentId === zielId)
? zielId
: agenten[0].agentId;
for (const a of agenten) {
const eintrag = erstelleElement("div", {
klasse: `agenten-karte ${a.agentId === auswahlId ? "aktiv" : ""}`,
onclick: () => {
document.querySelectorAll(".agenten-karte").forEach((k) => k.classList.remove("aktiv"));
eintrag.classList.add("aktiv");
rendereDetails(a.agentId);
}
});
const top = erstelleElement("div", { klasse: "agent-karte-top" });
const name = erstelleElement("strong", { text: a.displayName });
const status = a.isRunning
? erstellePlakette(t("status.busy"), "warnung")
: erstellePlakette(t("status.ready"), "neutral");
top.append(name, status);
const meta = erstelleElement("div", { klasse: "agent-karte-meta" });
meta.append(
erstelleElement("span", { klasse: "gedaempft", text: `${t("agents.id")}: ${a.agentId}` }),
erstelleElement("span", { klasse: "gedaempft", text: ` · ${t("agents.model")}: ${a.model}` })
);
const werkzeugeZaehler = a.tools ? a.tools.filter((t) => t.enabled).length : 0;
const werkzeugeText = erstelleElement("div", {
klasse: "agent-karte-werkzeuge gedaempft",
text: t("agents.toolsActive", { count: werkzeugeZaehler })
});
eintrag.append(top, meta, werkzeugeText);
listeContainer.append(eintrag);
}
rendereDetails(auswahlId);
} catch (err) {
const text = err.instanzUnerreichbar
? (t("instances.instanceOfflineError") || err.message)
: (err.instanzKonflikt
? (t("instances.noInstanceActiveError") || err.message)
: err.message);
listeContainer.replaceChildren(
erstelleElement("div", { klasse: "fehler-zentriert", text })
);
}
}
async function rendereDetails(agentId) {
gewaehlterAgentId = agentId;
const detailContainer = document.querySelector(".agenten-detail-bereich");
if (!detailContainer) return;
detailContainer.replaceChildren();
if (!agentId) {
detailContainer.append(
erstelleKarte(t("agents.details"), [
erstelleElement("p", { klasse: "gedaempft", text: t("agents.selectPrompt") })
])
);
return;
}
const agent = agenten.find((a) => a.agentId === agentId);
if (!agent) return;
// Kopf-Aktionen (Chatten, Löschen)
const aktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
const zumChatBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach",
title: t("agents.chatTooltip"),
onclick: () => {
window.location.hash = `#/chat/${encodeURIComponent(agent.agentId)}`;
}
},
erstelleIcon(PFADE.chat, 14),
erstelleElement("span", { text: ` ${t("agents.chat")}` })
);
const loeschenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche gefahr flach",
title: t("agents.deleteTooltip"),
onclick: () => loescheAgent(agent.agentId, agent.displayName)
},
erstelleIcon(PFADE.muelleimer, 14),
erstelleElement("span", { text: ` ${t("agents.delete")}` })
);
aktionen.append(zumChatBtn, loeschenBtn);
// Stammdaten-Karte
const stammdatenListe = erstelleElement("dl", { klasse: "datenliste" });
const felder = [
[t("agents.name"), agent.displayName],
[t("agents.id"), agent.agentId],
[t("agents.model"), agent.model],
[t("agents.description"), agent.description || "—"]
];
for (const [bez, wert] of felder) {
stammdatenListe.append(
erstelleElement("dt", { text: bez }),
erstelleElement("dd", { text: wert })
);
}
const basisKarte = erstelleKarte(agent.displayName, [stammdatenListe], { aktionen });
// Werkzeuge-Karte
const werkzeugeListe = erstelleElement("div", { klasse: "werkzeuge-liste" });
if (!agent.tools || agent.tools.length === 0) {
werkzeugeListe.append(
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noTools") })
);
} else {
for (const tool of agent.tools) {
const zeile = erstelleElement("div", { klasse: "werkzeug-zeile" });
const links = erstelleElement("div", { klasse: "werkzeug-info" });
const toolTitel = erstelleElement("strong", { text: tool.name });
// #715: A tool that brings in outside text changes the agent's reporting line.
if (tool.readsForeignContent) {
toolTitel.append(
" ",
erstelleElement("span", { klasse: "werkzeug-fremd", text: t("agents.readsForeign") })
);
}
const toolDesc = erstelleElement("div", {
klasse: "werkzeug-beschreibung gedaempft",
text: tool.description || ""
});
links.append(toolTitel, toolDesc);
const rechts = erstelleElement("div", { klasse: "werkzeug-aktionen" });
const einstellungenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche flach klein",
title: t("agents.configTooltip"),
onclick: () => zeigeWerkzeugEinstellungen(agent.agentId, tool.name)
},
erstelleIcon(PFADE.einstellungen, 14),
erstelleElement("span", { text: ` ${t("agents.configuration")}` })
);
const toggleLabel = erstelleElement("label", { klasse: "schalter" });
const toggleInput = erstelleElement("input", {
type: "checkbox",
checked: Boolean(tool.enabled),
onchange: async (e) => {
const aktiv = e.target.checked;
try {
await api(
`/agents/${encodeURIComponent(agent.agentId)}/tools/${encodeURIComponent(tool.name)}?enabled=${aktiv}`,
{ method: "PUT" }
);
tool.enabled = aktiv;
aktualisiereBerichtsweg();
zeigeMeldung(
t("agents.toolToggled", {
tool: tool.name,
status: aktiv ? t("agents.enabled") : t("agents.disabled")
}),
"info"
);
} catch (err) {
e.target.checked = !aktiv;
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
}
}
});
const toggleSpan = erstelleElement("span", { klasse: "schalter-regler" });
toggleLabel.append(toggleInput, toggleSpan);
rechts.append(einstellungenBtn, toggleLabel);
zeile.append(links, rechts);
werkzeugeListe.append(zeile);
}
}
const werkzeugeKarte = erstelleKarte(t("agents.availableTools"), [werkzeugeListe]);
// #715: The reporting line — the tier follows from the tools; the setting can only tighten it.
const berichtswegText = erstelleElement("p", { klasse: "gedaempft" });
const aktualisiereBerichtsweg = () => {
const quellen = (agent.tools || [])
.filter((w) => w.enabled && w.readsForeignContent)
.map((w) => w.name);
if (agent.treatAsForeignReader) quellen.push(t("agents.settingSource"));
berichtswegText.textContent = quellen.length > 0
? t("agents.tierForeign", { sources: quellen.join(", ") })
: t("agents.tierStandard");
};
aktualisiereBerichtsweg();
const verschaerfenLabel = erstelleElement("label", { klasse: "schalter" });
const verschaerfenInput = erstelleElement("input", {
type: "checkbox",
checked: Boolean(agent.treatAsForeignReader),
onchange: async (e) => {
const wert = e.target.checked;
try {
await api(
`/agents/${encodeURIComponent(agent.agentId)}/foreign-reader?value=${wert}`,
{ method: "PUT" }
);
agent.treatAsForeignReader = wert;
aktualisiereBerichtsweg();
zeigeMeldung(t("agents.foreignReaderSaved", { name: agent.displayName }), "info");
} catch (err) {
e.target.checked = !wert;
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
}
}
});
verschaerfenLabel.append(verschaerfenInput, erstelleElement("span", { klasse: "schalter-regler" }));
const verschaerfenZeile = erstelleElement(
"div",
{ klasse: "werkzeug-zeile" },
erstelleElement(
"div",
{ klasse: "werkzeug-info" },
erstelleElement("strong", { text: t("agents.treatAsForeignReader") }),
erstelleElement("div", { klasse: "werkzeug-beschreibung gedaempft", text: t("agents.treatAsForeignReaderHint") })
),
erstelleElement("div", { klasse: "werkzeug-aktionen" }, verschaerfenLabel)
);
const berichtswegKarte = erstelleKarte(t("agents.reportingLine"), [berichtswegText, verschaerfenZeile]);
detailContainer.append(basisKarte, werkzeugeKarte, berichtswegKarte);
}
function oeffneNeuerAgentDialog() {
const formular = erstelleElement("form", {
klasse: "formular-raster",
id: "neuer-agent-formular"
});
const idFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: t("agents.idPlaceholder"),
id: "neu-agent-id"
});
const nameFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: t("agents.namePlaceholder"),
id: "neu-agent-name"
});
const modellFeld = erstelleElement("input", {
type: "text",
required: true,
placeholder: "z.B. anthropic/claude-3.5-sonnet",
value: "anthropic/claude-3.5-sonnet",
id: "neu-agent-modell"
});
const descFeld = erstelleElement("textarea", {
rows: "3",
placeholder: t("agents.descPlaceholder"),
id: "neu-agent-desc"
});
formular.append(
erstelleElement("label", { text: t("agents.idLabel") }),
idFeld,
erstelleElement("label", { text: t("agents.nameLabel") }),
nameFeld,
erstelleElement("label", { text: t("agents.modelLabel") }),
modellFeld,
erstelleElement("label", { text: t("agents.descLabel") }),
descFeld
);
const abbrechenBtn = erstelleElement(
"button",
{
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen()
},
erstelleElement("span", { text: t("common.cancel") })
);
const speichernBtn = erstelleElement(
"button",
{
type: "submit",
klasse: "schaltflaeche primaer",
form: "neuer-agent-formular",
onclick: () => {
formular.requestSubmit();
}
},
erstelleElement("span", { text: t("agents.createButton") })
);
const modal = erstelleModal(t("agents.newAgentTitle"), formular, [abbrechenBtn, speichernBtn]);
formular.addEventListener("submit", async (e) => {
e.preventDefault();
speichernBtn.disabled = true;
try {
await api("/agents", {
method: "POST",
body: {
agentId: idFeld.value.trim(),
displayName: nameFeld.value.trim(),
model: modellFeld.value.trim(),
description: descFeld.value.trim()
}
});
modal.schliessen();
zeigeMeldung(t("agents.createdSuccess", { name: nameFeld.value.trim() }), "erfolg");
await ladeUndRendereListe(idFeld.value.trim());
} catch (err) {
zeigeMeldung(t("agents.createFailed", { error: err.message }), "gefahr");
} finally {
speichernBtn.disabled = false;
}
});
modal.oeffnen();
}
async function loescheAgent(agentId, name) {
if (!confirm(t("agents.deleteConfirm", { name, id: agentId }))) return;
try {
await api(`/agents/${encodeURIComponent(agentId)}`, { method: "DELETE" });
zeigeMeldung(t("agents.deleted", { name }), "info");
await ladeUndRendereListe();
} catch (err) {
zeigeMeldung(t("agents.deleteFailed", { error: err.message }), "gefahr");
}
}
async function zeigeWerkzeugEinstellungen(agentId, toolName) {
const box = erstelleElement("div", { klasse: "einstellungen-box" });
box.append(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadingSettings") }));
const schliessenBtn = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach",
onclick: () => modal.schliessen(),
text: t("common.close")
});
const modal = erstelleModal(
t("agents.settingsTitle", { tool: toolName, agent: agentId }),
box,
[schliessenBtn]
);
modal.oeffnen();
try {
const daten = await api(
`/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/settings`
);
box.replaceChildren();
const hinweis = erstelleElement("p", {
klasse: "hinweis",
text: t("agents.secretHint")
});
box.append(hinweis);
if (!daten || Object.keys(daten).length === 0) {
box.append(
erstelleElement("p", { klasse: "gedaempft", text: t("agents.noSettings") })
);
} else {
const dl = erstelleElement("dl", { klasse: "datenliste" });
for (const [schluessel, wert] of Object.entries(daten)) {
const dt = erstelleElement("dt", { text: schluessel });
const dd = erstelleElement("dd", { text: typeof wert === "object" ? JSON.stringify(wert) : String(wert) });
dl.append(dt, dd);
}
box.append(dl);
}
} catch (err) {
box.replaceChildren(
erstelleElement("p", { klasse: "fehler", text: t("agents.loadFailed", { error: err.message }) })
);
}
const rechteBox = erstelleElement("div", { klasse: "einstellungen-box" });
box.after(rechteBox);
await zeigeWerkzeugrechte(agentId, toolName, rechteBox);
}
function getWirkung(effective) {
const map = {
auto: [t("agents.effectAuto"), "erfolg"],
approve: [t("agents.effectApprove"), "warnung"],
deny: [t("agents.effectDeny"), "gefahr"]
};
return map[effective] ?? [effective, "neutral"];
}
/**
* #641: Rechte je Operation. "erlaubt" laeuft ohne Rueckfrage, "freigabe" wird einzeln
* vorgelegt, "verboten" gar nicht erst. Die Wirkung zeigt, was tatsaechlich geschieht -
* auch durch Dauerfreigaben oder die Vorgabe des Werkzeugs.
*/
async function zeigeWerkzeugrechte(agentId, toolName, box) {
const pfad = `/agents/${encodeURIComponent(agentId)}/tools/${encodeURIComponent(toolName)}/rights`;
let rechte;
try {
rechte = await api(pfad);
} catch (err) {
box.replaceChildren(erstelleElement("p", { klasse: "gedaempft", text: t("agents.loadFailed", { error: err.message }) }));
return;
}
box.replaceChildren(erstelleElement("h3", { text: t("agents.rights") }));
if (!rechte.operations || rechte.operations.length === 0) {
box.append(
erstelleElement("p", {
klasse: "gedaempft",
text: t("agents.noOperations")
})
);
return;
}
box.append(
erstelleElement("p", {
klasse: "hinweis",
text: rechte.configured
? t("agents.configuredHint")
: t("agents.notConfiguredHint")
})
);
const tabelle = erstelleElement("table", { klasse: "daten-tabelle rechte-tabelle" });
const kopf = erstelleElement("tr", {});
for (const titel of [t("agents.operation"), t("agents.setting"), t("agents.effect")]) {
kopf.append(erstelleElement("th", { text: titel }));
}
tabelle.append(kopf);
const auswahl = new Map();
for (const op of rechte.operations) {
const select = erstelleElement(
"select",
{ disabled: op.alwaysApproval },
erstelleElement("option", { value: "erlaubt", text: t("agents.allowed") }),
erstelleElement("option", { value: "freigabe", text: t("agents.singleApproval") }),
erstelleElement("option", { value: "verboten", text: t("agents.forbidden") })
);
// Ohne gesetzte Rechte die tatsaechliche Wirkung vorbelegen - sonst wuerde das erste
// Speichern z.B. das freie Lesen still in eine Freigabepflicht verwandeln.
select.value = rechte.configured
? op.setting
: op.effective === "auto" ? "erlaubt" : op.effective === "deny" ? "verboten" : "freigabe";
auswahl.set(op.operation, select);
const [wirkungText, wirkungArt] = getWirkung(op.effective);
const wirkung = erstelleElement("td", {}, erstellePlakette(wirkungText, wirkungArt));
if (op.standingApprovalId) {
wirkung.append(" ", erstellePlakette(t("agents.standingApproval", { id: op.standingApprovalId }), "neutral"));
}
wirkung.append(erstelleElement("div", {
klasse: "grund",
text: op.alwaysApproval ? t("agents.alwaysApprovalReason") : op.reason
}));
const zeile = erstelleElement("tr", {});
zeile.append(erstelleElement("td", { text: op.operation }), erstelleElement("td", {}, select), wirkung);
tabelle.append(zeile);
}
box.append(tabelle);
const speichern = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer klein",
text: t("agents.saveRights"),
onclick: async () => {
const allowed = [];
const denied = [];
for (const [operation, select] of auswahl) {
if (select.disabled) continue;
if (select.value === "erlaubt") allowed.push(operation);
if (select.value === "verboten") denied.push(operation);
}
speichern.disabled = true;
try {
const res = await api(pfad, { method: "PUT", body: { allowed, denied } });
zeigeMeldung(res?.message ?? t("agents.rightsSaved"), "erfolg");
await zeigeWerkzeugrechte(agentId, toolName, box);
} catch (err) {
zeigeMeldung(t("agents.saveFailed", { error: err.message }), "gefahr");
speichern.disabled = false;
}
}
});
box.append(speichern);
}