Fremde Laufzeiten: Claude Code und Befehl in der Sandbox, Egress-Proxy, Vorschlag statt Einsatz (#712)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-25 22:46:49 +02:00
co-authored by Claude Opus 5.5
parent 7d6c2c9495
commit 367ea387f6
40 changed files with 3428 additions and 29 deletions
@@ -869,6 +869,21 @@ button.klein {
min-width: 0;
}
/* #712: the runtime card */
.laufzeit-feld {
display: grid;
gap: 6px;
}
.laufzeit-feld[hidden],
.laufzeit-konto[hidden] {
display: none;
}
.laufzeit-konto {
margin-top: 12px;
}
.mcp-token-inaktiv {
opacity: 0.55;
}
@@ -945,5 +945,42 @@
"mcp.configHint": "In die .mcp.json des Projekts, in dem du arbeitest. Sie enthält das Token: die Datei nicht in die Versionsverwaltung aufnehmen.",
"mcp.copy": "Kopieren",
"mcp.copied": "Kopiert.",
"mcp.copyFailed": "Kopieren ist hier nicht erlaubt — Text markieren und von Hand kopieren."
"mcp.copyFailed": "Kopieren ist hier nicht erlaubt — Text markieren und von Hand kopieren.",
"runtime.title": "Laufzeit",
"runtime.hint": "Native Läufe nutzen unsere Engine mit OpenRouter. Eine fremde Laufzeit arbeitet in der Sandbox auf einer Kopie des Arbeitsbereichs: Was sie ändert, kommt als Vorschlag zurück und wird nie von selbst übernommen. Taskboard und Kollegen erreicht sie nur über den MCP-Server dieser Instanz.",
"runtime.kind": "Laufzeit",
"runtime.kind.native": "Nativ (OpenRouter)",
"runtime.kind.claude-code": "Claude Code",
"runtime.kind.command": "Befehl",
"runtime.allowedTools": "Erlaubte Werkzeuge",
"runtime.allowedToolsHint": "Durch Komma getrennt. Leer: Read, Edit, Write, Glob, Grep und der MCP-Server. Was nicht aufgeführt ist, wird abgelehnt; Webzugriff immer.",
"runtime.maxTurns": "Züge je Lauf",
"runtime.timeoutMinutes": "Zeitgrenze (Minuten)",
"runtime.command": "Befehl",
"runtime.commandHint": "Absolut, im PATH der Sandbox oder relativ zum Arbeitsbereich. Die Nachricht kommt über stdin; was der Befehl ausgibt, ist das Ergebnis.",
"runtime.arguments": "Argumente",
"runtime.argumentsHint": "Eines je Zeile.",
"runtime.egress": "Erreichbare Hosts",
"runtime.egressHint": "Einer je Zeile, z. B. api.example.com oder *.example.com:8443. Nur HTTPS, nur öffentliche Adressen. Die API von Claude Code ist immer dabei.",
"runtime.saved": "Laufzeit von {name} gespeichert.",
"runtime.failed": "Fehlgeschlagen: {error}",
"runtime.sandboxReady": "Die Sandbox ist bereit.",
"runtime.sandboxMissing": "Keine Sandbox auf dieser Maschine – fremde Laufzeiten laufen nicht: {reason}",
"runtime.account": "Claude-Code-Konto",
"runtime.accountHint": "Ein Konto für die ganze Instanz. Voreinstellung ist der API-Schlüssel; ein Abo teilt sein Kontingent mit dir, und ob es ein Agentensystem betreiben darf, regeln seine Bedingungen. Schlüssel werden verschlüsselt gespeichert und nie wieder angezeigt.",
"runtime.installedAt": "installiert unter {path}",
"runtime.notInstalled": "nicht installiert",
"runtime.authApiKey": "API-Schlüssel",
"runtime.authSubscription": "Abo",
"runtime.keyStored": "Schlüssel hinterlegt",
"runtime.keyMissing": "noch kein Schlüssel",
"runtime.editAccount": "Bearbeiten",
"runtime.executable": "Programm",
"runtime.auth": "Anmeldung",
"runtime.apiKey": "API-Schlüssel",
"runtime.oauthToken": "Abo-Token",
"runtime.oauthTokenHint": "Erzeugt mit 'claude setup-token' auf einem Rechner, an dem du angemeldet bist.",
"runtime.keepStored": "hinterlegt – leer lassen zum Behalten",
"runtime.accountSaved": "Claude-Code-Konto gespeichert."
}
@@ -945,5 +945,42 @@
"mcp.configHint": "Put this into the .mcp.json of the project you work in. It contains the token: keep the file out of version control.",
"mcp.copy": "Copy",
"mcp.copied": "Copied.",
"mcp.copyFailed": "Copying is not allowed here — select the text and copy it by hand."
"mcp.copyFailed": "Copying is not allowed here — select the text and copy it by hand.",
"runtime.title": "Runtime",
"runtime.hint": "Native runs use our engine with OpenRouter. A foreign runtime works in the sandbox on a copy of the workspace: what it changes comes back as a proposal and is never applied by itself. It reaches the taskboard and colleagues only through this instance's MCP server.",
"runtime.kind": "Runtime",
"runtime.kind.native": "Native (OpenRouter)",
"runtime.kind.claude-code": "Claude Code",
"runtime.kind.command": "Command",
"runtime.allowedTools": "Allowed tools",
"runtime.allowedToolsHint": "Comma-separated. Empty: Read, Edit, Write, Glob, Grep and the MCP server. Anything not listed is denied; web access is always denied.",
"runtime.maxTurns": "Turns per run",
"runtime.timeoutMinutes": "Time limit (minutes)",
"runtime.command": "Command",
"runtime.commandHint": "Absolute, on the sandbox's PATH, or relative to the workspace. The message arrives on stdin; what the command prints is the result.",
"runtime.arguments": "Arguments",
"runtime.argumentsHint": "One per line.",
"runtime.egress": "Reachable hosts",
"runtime.egressHint": "One per line, e.g. api.example.com or *.example.com:8443. Only HTTPS, only public addresses. Claude Code's API is always included.",
"runtime.saved": "Runtime of {name} saved.",
"runtime.failed": "Failed: {error}",
"runtime.sandboxReady": "The sandbox is ready.",
"runtime.sandboxMissing": "No sandbox on this machine — foreign runtimes do not run: {reason}",
"runtime.account": "Claude Code account",
"runtime.accountHint": "One account for the whole instance. The API key is the default; a subscription shares its quota with you, and whether it may run an agent system is up to its terms. Keys are stored encrypted and never shown again.",
"runtime.installedAt": "installed at {path}",
"runtime.notInstalled": "not installed",
"runtime.authApiKey": "API key",
"runtime.authSubscription": "Subscription",
"runtime.keyStored": "key stored",
"runtime.keyMissing": "no key yet",
"runtime.editAccount": "Edit",
"runtime.executable": "Program",
"runtime.auth": "Sign-in",
"runtime.apiKey": "API key",
"runtime.oauthToken": "Subscription token",
"runtime.oauthTokenHint": "Created with 'claude setup-token' on a machine where you are signed in.",
"runtime.keepStored": "stored — leave empty to keep",
"runtime.accountSaved": "Claude Code account saved."
}
@@ -6,6 +6,7 @@ import { api } from "../modules/api.js";
import { abonnieren } from "../modules/events.js";
import { t } from "../modules/i18n.js";
import { erstelleMcpKarte } from "./mcp-access.js";
import { erstelleLaufzeitKarte } from "./runtime.js";
import {
erstelleElement,
erstelleIcon,
@@ -331,7 +332,8 @@ async function rendereDetails(agentId) {
const berichtswegKarte = erstelleKarte(t("agents.reportingLine"), [berichtswegText, verschaerfenZeile]);
// #713: A human's own client working as this agent, through the MCP server.
detailContainer.append(basisKarte, werkzeugeKarte, berichtswegKarte, erstelleMcpKarte(agent));
// #712: native, or a foreign runtime in the sandbox.
detailContainer.append(basisKarte, werkzeugeKarte, berichtswegKarte, erstelleLaufzeitKarte(agent), erstelleMcpKarte(agent));
}
function oeffneNeuerAgentDialog() {
@@ -0,0 +1,180 @@
// ClawdDotNet — where an agent's runs happen (#712)
//
// Native is our engine with OpenRouter. A foreign runtime (Claude Code, a command) runs in
// the sandbox, on a copy of the workspace; what it changes comes back as a proposal and is
// never applied by itself. Its only way to the taskboard and to colleagues is our MCP server.
import { api } from "../modules/api.js";
import { t } from "../modules/i18n.js";
import { erstelleElement, erstelleKarte, erstelleModal, zeigeMeldung } from "../modules/dom.js";
const liste = (text) => text.split(/[\n,]/).map((x) => x.trim()).filter((x) => x.length > 0);
/** The card for one agent: its runtime and, for Claude Code, the instance's account. */
export function erstelleLaufzeitKarte(agent) {
const laufzeit = agent.runtime || { kind: "native", allowedTools: [], maxTurns: 30, timeoutMinutes: 30, command: "", arguments: [], egress: [] };
const lage = erstelleElement("p", { klasse: "gedaempft text-klein" });
const konto = erstelleElement("div", { klasse: "laufzeit-konto" });
const art = erstelleElement("select", { "aria-label": t("runtime.kind") });
for (const kind of ["native", "claude-code", "command"]) {
art.append(erstelleElement("option", { value: kind, text: t(`runtime.kind.${kind}`) }));
}
art.value = laufzeit.kind;
const werkzeuge = erstelleElement("textarea", { rows: "2", placeholder: "Read, Edit, Write, Glob, Grep, mcp__clawd" });
werkzeuge.value = (laufzeit.allowedTools || []).join(", ");
const zuege = erstelleElement("input", { type: "number", min: "1", max: "500", value: String(laufzeit.maxTurns || 30) });
const minuten = erstelleElement("input", { type: "number", min: "1", max: "240", value: String(laufzeit.timeoutMinutes || 30) });
const befehl = erstelleElement("input", { type: "text", value: laufzeit.command || "", placeholder: "scripts/run.sh" });
const argumente = erstelleElement("textarea", { rows: "2" });
argumente.value = (laufzeit.arguments || []).join("\n");
const ziele = erstelleElement("textarea", { rows: "2", placeholder: "api.example.com" });
ziele.value = (laufzeit.egress || []).join("\n");
const feld = (schluessel, element, hinweis) => erstelleElement("div", { klasse: "laufzeit-feld", "data-feld": schluessel },
erstelleElement("label", { text: t(`runtime.${schluessel}`) }),
element,
hinweis ? erstelleElement("div", { klasse: "gedaempft text-klein", text: hinweis }) : null);
const felder = {
werkzeuge: feld("allowedTools", werkzeuge, t("runtime.allowedToolsHint")),
zuege: feld("maxTurns", zuege),
minuten: feld("timeoutMinutes", minuten),
befehl: feld("command", befehl, t("runtime.commandHint")),
argumente: feld("arguments", argumente, t("runtime.argumentsHint")),
ziele: feld("egress", ziele, t("runtime.egressHint"))
};
const sichtbar = () => {
const kind = art.value;
const fremd = kind !== "native";
felder.werkzeuge.hidden = kind !== "claude-code";
felder.zuege.hidden = kind !== "claude-code";
felder.minuten.hidden = !fremd;
felder.befehl.hidden = kind !== "command";
felder.argumente.hidden = kind !== "command";
felder.ziele.hidden = !fremd;
konto.hidden = kind !== "claude-code";
};
art.addEventListener("change", sichtbar);
const speichern = erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer",
text: t("common.save"),
onclick: async () => {
speichern.disabled = true;
const eingabe = {
kind: art.value,
allowedTools: liste(werkzeuge.value),
maxTurns: Number(zuege.value) || 30,
timeoutMinutes: Number(minuten.value) || 30,
command: befehl.value.trim(),
arguments: argumente.value.split("\n").map((x) => x.trim()).filter((x) => x.length > 0),
egress: liste(ziele.value)
};
try {
await api(`/agents/${encodeURIComponent(agent.agentId)}/runtime`, { method: "PUT", body: eingabe });
agent.runtime = eingabe;
zeigeMeldung(t("runtime.saved", { name: agent.displayName }), "info");
} catch (err) {
zeigeMeldung(t("runtime.failed", { error: err.message }), "gefahr");
} finally {
speichern.disabled = false;
}
}
});
async function lade() {
try {
const stand = await api("/runtimes");
lage.textContent = stand.sandboxAvailable
? t("runtime.sandboxReady")
: t("runtime.sandboxMissing", { reason: stand.sandboxReason || "" });
zeigeKonto(stand.claudeCode);
} catch (err) {
lage.textContent = err.message;
}
}
function zeigeKonto(claude) {
const anmeldung = claude.auth === "subscription" ? t("runtime.authSubscription") : t("runtime.authApiKey");
const schluessel = claude.auth === "subscription" ? claude.hasOAuthToken : claude.hasApiKey;
konto.replaceChildren(
erstelleElement("div", { klasse: "werkzeug-zeile" },
erstelleElement("div", { klasse: "werkzeug-info" },
erstelleElement("strong", { text: t("runtime.account") }),
erstelleElement("div", {
klasse: "werkzeug-beschreibung gedaempft",
text: [
claude.located ? t("runtime.installedAt", { path: claude.located }) : t("runtime.notInstalled"),
anmeldung,
schluessel ? t("runtime.keyStored") : t("runtime.keyMissing")
].join(" · ")
})),
erstelleElement("div", { klasse: "werkzeug-aktionen" },
erstelleElement("button", {
type: "button",
klasse: "schaltflaeche flach klein",
text: t("runtime.editAccount"),
onclick: () => bearbeiteKonto(claude, lade)
}))));
}
sichtbar();
lade();
return erstelleKarte(t("runtime.title"), [
erstelleElement("p", { klasse: "gedaempft", text: t("runtime.hint") }),
lage,
erstelleElement("div", { klasse: "formular-raster" },
erstelleElement("label", { text: t("runtime.kind") }), art,
felder.werkzeuge, felder.zuege, felder.minuten, felder.befehl, felder.argumente, felder.ziele),
konto,
erstelleElement("div", { klasse: "modal-fuss" }, speichern)
]);
}
/** The instance's Claude Code account: program, sign-in, key. A key is only ever replaced. */
function bearbeiteKonto(claude, danach) {
const programm = erstelleElement("input", { type: "text", value: claude.executable || "", placeholder: claude.located || "~/.local/bin/claude" });
const anmeldung = erstelleElement("select");
anmeldung.append(
erstelleElement("option", { value: "api-key", text: t("runtime.authApiKey") }),
erstelleElement("option", { value: "subscription", text: t("runtime.authSubscription") }));
anmeldung.value = claude.auth || "api-key";
const schluessel = erstelleElement("input", { type: "password", autocomplete: "off", placeholder: claude.hasApiKey ? t("runtime.keepStored") : "" });
const token = erstelleElement("input", { type: "password", autocomplete: "off", placeholder: claude.hasOAuthToken ? t("runtime.keepStored") : "" });
const modal = erstelleModal(t("runtime.account"), erstelleElement("div", {},
erstelleElement("p", { klasse: "gedaempft text-klein", text: t("runtime.accountHint") }),
erstelleElement("div", { klasse: "formular-raster" },
erstelleElement("label", { text: t("runtime.executable") }), programm,
erstelleElement("label", { text: t("runtime.auth") }), anmeldung,
erstelleElement("label", { text: t("runtime.apiKey") }), schluessel,
erstelleElement("label", { text: t("runtime.oauthToken") }), token,
erstelleElement("div", { klasse: "gedaempft text-klein", text: t("runtime.oauthTokenHint") }))
), [
erstelleElement("button", { type: "button", klasse: "schaltflaeche flach", text: t("common.cancel"), onclick: () => modal.schliessen() }),
erstelleElement("button", {
type: "button",
klasse: "schaltflaeche primaer",
text: t("common.save"),
onclick: async () => {
try {
await api("/runtimes/claude-code", {
method: "PUT",
body: { executable: programm.value.trim(), auth: anmeldung.value, apiKey: schluessel.value, oauthToken: token.value }
});
zeigeMeldung(t("runtime.accountSaved"), "info");
modal.schliessen();
await danach();
} catch (err) {
zeigeMeldung(t("runtime.failed", { error: err.message }), "gefahr");
}
}
})
]);
modal.oeffnen();
}