Fix MoA single-round and writable read-only panel members

Two bugs surfaced when driving the composite toggle (off→plan→fusion→moa)
in an all-CLI setup (no API keys):

1. MoA ran only 1 round instead of the configured 2. cycleCompositeMode
   fired the multiModelMode PATCH un-serialized and sendMessage did not
   await it, so a message sent right after toggling loaded the session
   before 'moa' was committed → the backend read the prior 'fusion' value
   → 1 round. Persisted totalLayers in the DB confirmed fusion (1) for all
   post-toggle chains. Serialize mode PATCHes into a promise chain (last
   click wins, in order) and await it in sendMessage/sendMessageDirect.

2. A read-only panel member could still write files. ClaudeCliService
   hard-coded --permission-mode acceptEdits, which auto-accepts Write/Edit
   regardless of --allowed-tools, so a member running with planMode=true
   (tools limited to Read/Glob/Grep) could still call write_file. Use
   'plan' when planMode is set; this also closes the same latent hole for
   regular plan-mode users on the shared CLI path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-19 16:55:25 +02:00
1 parent e3f89043c1
commit 2501096e19
2 files changed
+31 -9

No files matched your search

+7 -1
View File
@@ -159,6 +159,12 @@ public class ClaudeCliService
if (!_isAvailable)
return new CliChatResult { Success = false, Error = "Claude CLI nicht verfügbar" };
// Permission-Mode: Im Plan-Modus (auch Panel-Member laufen read-only via planMode=true)
// MUSS die Schreib-Sperre über die Permission-Ebene erzwungen werden. 'acceptEdits' würde
// Write/Edit AUTOMATISCH akzeptieren – UNABHÄNGIG von --allowed-tools – sodass ein als
// read-only gedachter Member (oder ein regulärer Plan-Modus-User) trotzdem Dateien schreiben
// könnte. 'plan' blockiert alle Schreib-/Edit-/Bash-Operationen auf Permission-Ebene.
var permissionMode = planMode ? "plan" : "acceptEdits";
var args = new List<string>
{
"-p",
@@ -166,7 +172,7 @@ public class ClaudeCliService
"--output-format", "stream-json",
"--include-partial-messages",
"--no-session-persistence",
"--permission-mode", "acceptEdits",
"--permission-mode", permissionMode,
"--disallowed-tools", "AskUserQuestion"
};
+24 -8
View File
@@ -236,6 +236,14 @@ let _planModeActive = false; // true genau wenn _compositeMode === 'plan'
const _COMPOSITE_ORDER = ['off', 'plan', 'fusion', 'moa'];
// Serialisierte Modus-Persistenz. Schnelles Durchklicken (off→plan→fusion→moa) feuert sonst
// ÜBERLAPPENDE PATCHes, deren Commit-Reihenfolge nicht garantiert ist → der zuletzt committete
// Zwischenwert 'fusion' kann 'moa' überschreiben. Und ein direkt danach gesendeter Chat-Request
// lädt die Session, BEVOR 'moa' committed ist → das Backend liest den alten Modus (Fusion → 1 Runde
// statt MoA). Die Kette garantiert (a) Reihenfolge (letzter Klick gewinnt) und (b) dass sendMessage
// das offene PATCH abwarten kann. Die Kette rejected nie (Fehler werden hier geschluckt).
let _modePersist = Promise.resolve();
function _renderCompositeBtn() {
var btn = $('#planModeBtn');
if (!btn) return;
@@ -281,17 +289,19 @@ async function cycleCompositeMode() {
_planModeActive = (_compositeMode === 'plan');
_renderCompositeBtn();
// Nur Fusion/MoA/Off brauchen einen Server-Call (Plan ist transient)
// Nur Fusion/MoA/Off brauchen einen Server-Call (Plan ist transient). Auf die serialisierte
// Kette legen, damit aufeinanderfolgende Klicks in Reihenfolge committen (letzter Klick gewinnt)
// und sendMessage das offene PATCH abwarten kann.
if (_compositeMode !== 'plan') {
var sid = getCurrentSessionId();
if (sid) {
try {
await apiUpdateSession(sid, { multiModelMode: _compositeMode });
} catch (e) {
console.error('Modus speichern fehlgeschlagen:', e);
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
return;
}
var targetMode = _compositeMode;
_modePersist = _modePersist
.then(function () { return apiUpdateSession(sid, { multiModelMode: targetMode }); })
.catch(function (e) {
console.error('Modus speichern fehlgeschlagen:', e);
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
});
}
}
@@ -580,6 +590,11 @@ async function sendMessage() {
input.value = '';
input.dispatchEvent(new Event('input'));
// Ein evtl. noch offenes Modus-PATCH (Fusion/MoA/Aus aus dem Toggle) MUSS committed sein,
// bevor der Chat-Request die Session lädt – sonst läuft die Chain mit dem alten Modus
// (z.B. MoA → nur 1 Runde, weil das Backend noch 'fusion' liest).
await _modePersist;
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive));
}
@@ -616,6 +631,7 @@ async function sendMessageDirect(text) {
if (isStreaming || !text) return;
addUserBubble(text, true);
scrollDownForce();
await _modePersist; // offenes Modus-PATCH abwarten (s. sendMessage)
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
}