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
@@ -1375,3 +1375,43 @@ button.klein {
color: var(--text);
}
/* ─── Hinweiskaesten (#608, #641) ─── */
.hinweis-box {
border: 1px solid var(--divider);
border-left-width: 4px;
background: var(--surface);
padding: 10px 14px;
margin: 10px 0;
font-size: 14px;
}
.hinweis-box.warnung {
border-color: var(--warnung);
background: rgba(181, 130, 42, 0.10);
}
.hinweis-box.vorschlag {
border-color: var(--erfolg);
background: rgba(63, 143, 92, 0.10);
}
/* ─── Rechte je Werkzeug (#641) ─── */
.rechte-tabelle select {
min-width: 150px;
}
.rechte-tabelle .grund {
font-size: 12px;
color: var(--gedaempft);
}
.gleichartig-wahl {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
font-size: 14px;
}
@@ -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);
}
@@ -46,7 +46,8 @@ export async function rendereStaging() {
);
const listeBereich = erstelleElement("div", { id: "staging-liste-bereich" });
layout.append(kopfZeile, listeBereich);
const dauerBereich = erstelleElement("div", { id: "staging-dauer-bereich" });
layout.append(kopfZeile, listeBereich, dauerBereich);
container.append(layout);
await ladeFreigaben();
@@ -66,6 +67,7 @@ async function ladeFreigaben() {
// Aktualisiere Badge in Sidebar
aktualisiereStagingBadge(wartendeAufrufe.length);
ladeDauerfreigaben();
if (!wartendeAufrufe || wartendeAufrufe.length === 0) {
container.append(
@@ -82,11 +84,15 @@ async function ladeFreigaben() {
for (const anfrage of wartendeAufrufe) {
const kopfAktionen = erstelleElement("div", { klasse: "gruppe-horizontal" });
// #641: "Gleichartiges kuenftig erlauben" - nie vorausgewaehlt, auch nicht beim
// Vorschlag. Uebernehmen muss der Mensch.
const gleichartig = erstelleElement("input", { type: "checkbox", id: `gleichartig-${anfrage.id}` });
const freigebenBtn = erstelleElement(
"button",
{
klasse: "schaltflaeche primaer klein",
onclick: () => bestaetigeFreigabe(anfrage.id)
onclick: () => bestaetigeFreigabe(anfrage.id, gleichartig.checked)
},
erstelleIcon(PFADE.haken, 14),
erstelleElement("span", { text: " Freigeben" })
@@ -128,7 +134,36 @@ async function ladeFreigaben() {
});
argBox.append(pre);
const karte = erstelleKarte(`${anfrage.tool}.${anfrage.action || "?"}`, [datenliste, argBox], {
const teile = [datenliste, argBox];
if (anfrage.suggestAllowSimilar) {
teile.push(
erstelleElement("div", {
klasse: "hinweis-box vorschlag",
text:
`Diese Operation von ${anfrage.agentId} wurde in den letzten 14 Tagen schon ` +
`${anfrage.previousApprovals}-mal freigegeben. Soll ${anfrage.tool}.${anfrage.action} ` +
`für diesen Agenten künftig ohne Rückfrage laufen?`
})
);
}
if (anfrage.canAllowSimilar) {
teile.push(
erstelleElement(
"label",
{ klasse: "gleichartig-wahl", for: `gleichartig-${anfrage.id}` },
gleichartig,
erstelleElement("span", {
text:
`Gleichartiges künftig erlauben: ${anfrage.agentId} darf ${anfrage.tool}.${anfrage.action} ` +
`ab jetzt ohne Rückfrage (widerrufbar unten unter „Dauerfreigaben“).`
})
)
);
}
const karte = erstelleKarte(`${anfrage.tool}.${anfrage.action || "?"}`, teile, {
aktionen: kopfAktionen
});
@@ -170,16 +205,87 @@ function aktualisiereStagingBadge(anzahl) {
}
}
async function bestaetigeFreigabe(id) {
async function bestaetigeFreigabe(id, gleichartig = false) {
try {
const res = await api(`/staging/${id}/approve`, { method: "POST" });
zeigeMeldung(`Aufruf freigegeben: ${res?.message ?? "Erfolgreich ausgeführt"}`, "erfolg");
const res = await api(`/staging/${id}/approve${gleichartig ? "?similar=true" : ""}`, { method: "POST" });
zeigeMeldung(
`Aufruf freigegeben${gleichartig ? " und künftig erlaubt" : ""}: ${res?.message ?? "Erfolgreich ausgeführt"}`,
"erfolg"
);
await ladeFreigaben();
} catch (err) {
zeigeMeldung(`Freigabe fehlgeschlagen: ${err.message}`, "gefahr");
}
}
/**
* #641: Die geltenden Dauerfreigaben - "Gleichartiges kuenftig erlauben" - mit Widerruf.
*/
async function ladeDauerfreigaben() {
const bereich = document.getElementById("staging-dauer-bereich");
if (!bereich) return;
let liste;
try {
liste = await api("/staging/standing");
} catch {
bereich.replaceChildren();
return;
}
const inhalt = [];
if (!Array.isArray(liste) || liste.length === 0) {
inhalt.push(
erstelleElement("p", {
klasse: "gedaempft",
text:
"Keine. Beim Freigeben lässt sich „Gleichartiges künftig erlauben“ wählen; " +
"dauerhafte Rechte stehen sonst in der Werkzeugkonfiguration des Agenten."
})
);
} else {
const tabelle = erstelleElement("table", { klasse: "daten-tabelle" });
const kopf = erstelleElement("tr", {});
for (const titel of ["Agent", "Operation", "Erteilt", "Anlass", ""]) {
kopf.append(erstelleElement("th", { text: titel }));
}
tabelle.append(kopf);
for (const d of liste) {
const zeile = erstelleElement("tr", {});
const widerrufen = erstelleElement(
"button",
{ klasse: "schaltflaeche gefahr flach klein", onclick: () => widerrufeDauerfreigabe(d) },
erstelleElement("span", { text: "Widerrufen" })
);
zeile.append(
erstelleElement("td", { text: d.agentId }),
erstelleElement("td", { text: `${d.tool}.${d.operation}` }),
erstelleElement("td", { text: `${formatiereDatumZeit(d.grantedAt)} (${d.grantedBy})` }),
erstelleElement("td", { text: d.sourceId ? `#${d.sourceId}` : "—" }),
erstelleElement("td", {}, widerrufen)
);
tabelle.append(zeile);
}
inhalt.push(tabelle);
}
bereich.replaceChildren(erstelleKarte("Dauerfreigaben", inhalt));
}
async function widerrufeDauerfreigabe(d) {
if (!confirm(`Dauerfreigabe widerrufen? ${d.agentId} muss ${d.tool}.${d.operation} dann wieder einzeln vorlegen.`)) {
return;
}
try {
const res = await api(`/staging/standing/${d.id}/revoke`, { method: "POST" });
zeigeMeldung(res?.message ?? "Widerrufen.", res?.success === false ? "gefahr" : "info");
await ladeDauerfreigaben();
} catch (err) {
zeigeMeldung(`Widerruf fehlgeschlagen: ${err.message}`, "gefahr");
}
}
function oeffneAblehnenDialog(id) {
const formular = erstelleElement("form", { klasse: "formular-raster" });
const grundInput = erstelleElement("input", {