Freigaben buendeln statt Weckflut (#643), Freigabemodell ueber Werkzeugrechte (#641)

#643: Entscheidungen wecken den Agenten nur noch bei Ablehnung oder gescheiterter
Ausfuehrung, gesammelt in einer Aufgabe je Agent (5 min Ruhefenster), frisch per
@new:agent statt im fortgefuehrten Chatverlauf. Eine einmalige Aufgabe, deren Lauf
wegen Budget oder Herunterfahren gar nicht stattfand, bleibt faellig (Wiedervorlage
nach 30 min) statt mit gesetztem Marker liegenzubleiben.

#641: Rechte stehen in der Werkzeugkonfiguration des Agenten (erlaubt/verboten).
Erlaubtes laeuft, Nicht-Erlaubtes wird einzeln vorgelegt statt verweigert, Verbotenes
gar nicht erst. "Gleichartiges kuenftig erlauben" erteilt eine widerrufbare
Dauerfreigabe je Agent/Werkzeug/Operation; nach drei gleichartigen Freigaben schlaegt
die Oberflaeche sie vor. Aenderungen an Agenten bleiben ausnahmslos freigabepflichtig.
Database: Operation ist das SQL-Schluesselwort; die eigene Zugriffspruefung greift nur
noch ohne Freigabestelle (AgentToolContext.FreigabeGeprueft). WebUI: Rechte-Tabelle je
Werkzeug, Dauerfreigaben mit Widerruf.

Version 0.1.11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-19 14:37:44 +02:00
co-authored by Claude Opus 5
parent f74e49ba35
commit 2f0361278b
36 changed files with 1687 additions and 137 deletions
@@ -411,19 +411,129 @@ async function zeigeWerkzeugEinstellungen(agentId, toolName) {
box.append(
erstelleElement("p", { klasse: "gedaempft", text: "Keine individuellen Einstellungen hinterlegt." })
);
return;
} 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);
}
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: `Laden fehlgeschlagen: ${err.message}` })
);
}
const rechteBox = erstelleElement("div", { klasse: "einstellungen-box" });
box.after(rechteBox);
await zeigeWerkzeugrechte(agentId, toolName, rechteBox);
}
const WIRKUNG = {
auto: ["läuft", "erfolg"],
approve: ["einzeln freigeben", "warnung"],
deny: ["gesperrt", "gefahr"]
};
/**
* #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: `Rechte nicht lesbar: ${err.message}` }));
return;
}
box.replaceChildren(erstelleElement("h3", { text: "Rechte" }));
if (!rechte.operations || rechte.operations.length === 0) {
box.append(
erstelleElement("p", {
klasse: "gedaempft",
text: "Dieses Werkzeug kennt keine einzelnen Operationen. Es gilt die Standardregel."
})
);
return;
}
box.append(
erstelleElement("p", {
klasse: "hinweis",
text: rechte.configured
? "Erlaubte Operationen laufen ohne Rückfrage, alle anderen werden einzeln zur Freigabe vorgelegt, verbotene gar nicht erst."
: "Noch keine Rechte gesetzt: Es gilt die Vorgabe des Werkzeugs. Mit dem Speichern werden die Einstellungen unten verbindlich."
})
);
const tabelle = erstelleElement("table", { klasse: "daten-tabelle rechte-tabelle" });
const kopf = erstelleElement("tr", {});
for (const titel of ["Operation", "Einstellung", "Wirkung"]) {
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: "erlaubt" }),
erstelleElement("option", { value: "freigabe", text: "einzeln freigeben" }),
erstelleElement("option", { value: "verboten", text: "verboten" })
);
// 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] = WIRKUNG[op.effective] ?? [op.effective, "neutral"];
const wirkung = erstelleElement("td", {}, erstellePlakette(wirkungText, wirkungArt));
if (op.standingApprovalId) {
wirkung.append(" ", erstellePlakette(`Dauerfreigabe #${op.standingApprovalId}`, "neutral"));
}
wirkung.append(erstelleElement("div", {
klasse: "grund",
text: op.alwaysApproval ? "Änderungen an Agenten brauchen immer eine Freigabe." : 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: "Rechte speichern",
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 ?? "Rechte gespeichert.", "erfolg");
await zeigeWerkzeugrechte(agentId, toolName, box);
} catch (err) {
zeigeMeldung(`Speichern fehlgeschlagen: ${err.message}`, "gefahr");
speichern.disabled = false;
}
}
});
box.append(speichern);
}