Werkzeugkasten: Zugangsdaten je Einheit (#692)

A unit holds credentials for its members, by tool and setting - needed once,
not once per agent, and other units never get them.

- UnitConfig.credentials, protected by ConfigSecrets like every secret.
- OrgChart.ToolSettingsFor: an agent's own settings, completed by the nearest
  unit up that holds the value; own non-empty values win; no tool is granted.
  Used for runs, tool jobs and preflight probes.
- OrgAdministration.SetUnitCredential, PUT /organisation/units/{id}/credentials;
  the view lists held settings, never values.
- Templates never carry unit credentials; they are named instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-01 12:50:27 +02:00
co-authored by Claude Opus 5.5
parent 661807b2d0
commit 9a265888f8
17 changed files with 306 additions and 13 deletions
@@ -100,6 +100,14 @@
"org.approvalsOperations": "Operationen",
"org.approvalsPerDay": "Höchstens je Tag",
"org.approvalsHours": "Stunden je Freigabe",
"org.credentials": "Zugangsdaten der Einheit",
"org.credentialsHint": "Zugangsdaten, die die Einheit für ihre Mitglieder und die Einheiten darunter hält — einmal gebraucht, nicht einmal je Agent, und andere Einheiten bekommen sie nie. Ein Mitglied nutzt sie nur für ein Werkzeug, das es hat, und nur, wo es keinen eigenen Wert trägt. Werte werden geschützt gespeichert und nie wieder angezeigt.",
"org.credentialsNone": "Die Einheit hält keine Zugangsdaten.",
"org.credentialSet": "gesetzt",
"org.credentialRemove": "Entfernen",
"org.credentialTool": "Werkzeug",
"org.credentialKey": "Einstellung",
"org.credentialValue": "Wert",
"org.doctrine": "Doktrin",
"org.doctrineHint": "Was in dieser Einheit gerade gilt — Rahmen für jede Aufgabe ihrer Mitglieder, von ihrer Leitung ohne Rückfrage gesetzt. Sie erreicht nur die Mitglieder dieser Einheit, nicht die Untereinheiten; die haben ihre eigene. Kurz halten: Sie geht in jeden Lauf. Leer entfernt sie.",
"org.doctrinePlaceholder": "Jeder Lieferantenkontakt wird in der Datenbank erfasst, bevor er weitergegeben wird.",
@@ -100,6 +100,14 @@
"org.approvalsOperations": "Operations",
"org.approvalsPerDay": "Per day at most",
"org.approvalsHours": "Hours per approval",
"org.credentials": "Credentials of the unit",
"org.credentialsHint": "Credentials the unit holds for its members and the units below — needed once, not once per agent, and other units never get them. A member uses them only for a tool it has, and only where it has no value of its own. Values are stored protected and never shown again.",
"org.credentialsNone": "The unit holds no credentials.",
"org.credentialSet": "set",
"org.credentialRemove": "Remove",
"org.credentialTool": "Tool",
"org.credentialKey": "Setting",
"org.credentialValue": "Value",
"org.doctrine": "Doctrine",
"org.doctrineHint": "What holds in this unit now — a frame for every task of its members, set by its lead without asking. It reaches this unit's members only, not the sub-units; they have their own. Short: it goes into every run. Empty removes it.",
"org.doctrinePlaceholder": "Every supplier contact is entered in the database before it is passed on.",
@@ -482,6 +482,7 @@ function unitPanel(unit) {
cards.push(unitWorkspaceCard(unit));
cards.push(ceilingCard(unit));
cards.push(credentialsCard(unit));
cards.push(approvalsCard(unit));
cards.push(doctrineCard(unit));
cards.push(inspectionCard(unit));
@@ -552,6 +553,43 @@ function ceilingCard(unit) {
]);
}
/**
* #692: Credentials the unit holds for its members — one marketplace key for six agents, and
* the accounts department never gets it. Values are written, never shown: only which settings
* are held is listed.
*/
function credentialsCard(unit) {
const held = unit.credentialKeys ?? {};
const tool = erstelleElement("input", { type: "text", placeholder: "DirectAPI" });
const key = erstelleElement("input", { type: "text", placeholder: "apiKey" });
const value = erstelleElement("input", { type: "password", autocomplete: "new-password" });
const path = `/organisation/units/${encodeURIComponent(unit.id)}/credentials`;
const rows = Object.entries(held).flatMap(([t1, keys]) => keys.map((k) => erstelleElement("li", { klasse: "gruppe-horizontal text-klein" },
erstelleElement("span", { text: `${t1}.${k}` }),
erstellePlakette(t("org.credentialSet"), "erfolg"),
erstelleElement("button", {
type: "button", klasse: "flach klein", text: t("org.credentialRemove"),
onclick: () => send(path, "PUT", { tool: t1, key: k, value: null, reason: reason() })
}))));
return erstelleKarte(t("org.credentials"), [
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.credentialsHint") }),
rows.length ? erstelleElement("ul", { klasse: "org-liste" }, ...rows) : erstelleElement("p", { klasse: "gedaempft text-klein", text: t("org.credentialsNone") }),
erstelleElement("div", { klasse: "formular-raster" },
erstelleElement("label", { text: t("org.credentialTool") }), tool,
erstelleElement("label", { text: t("org.credentialKey") }), key,
erstelleElement("label", { text: t("org.credentialValue") }), value),
erstelleElement("button", {
type: "button", klasse: "klein", text: t("common.save"),
onclick: async () => {
const ok = await send(path, "PUT", { tool: tool.value.trim(), key: key.value.trim(), value: value.value, reason: reason() });
if (ok) value.value = "";
}
})
]);
}
/**
* #687: What the unit's lead may approve instead of the human. One operation per line —
* "Tool.operation", or "Tool" for all of it. Changes to agents are refused by the server.