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:
co-authored by
Claude Opus 5.5
parent
7d6c2c9495
commit
367ea387f6
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user