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:
1 parent
e3f89043c1
commit
2501096e19
2 files changed
+31
-9
No files matched your search
@@ -159,6 +159,12 @@ public class ClaudeCliService
|
|||||||
if (!_isAvailable)
|
if (!_isAvailable)
|
||||||
return new CliChatResult { Success = false, Error = "Claude CLI nicht verfügbar" };
|
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>
|
var args = new List<string>
|
||||||
{
|
{
|
||||||
"-p",
|
"-p",
|
||||||
@@ -166,7 +172,7 @@ public class ClaudeCliService
|
|||||||
"--output-format", "stream-json",
|
"--output-format", "stream-json",
|
||||||
"--include-partial-messages",
|
"--include-partial-messages",
|
||||||
"--no-session-persistence",
|
"--no-session-persistence",
|
||||||
"--permission-mode", "acceptEdits",
|
"--permission-mode", permissionMode,
|
||||||
"--disallowed-tools", "AskUserQuestion"
|
"--disallowed-tools", "AskUserQuestion"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-8
@@ -236,6 +236,14 @@ let _planModeActive = false; // true genau wenn _compositeMode === 'plan'
|
|||||||
|
|
||||||
const _COMPOSITE_ORDER = ['off', 'plan', 'fusion', 'moa'];
|
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() {
|
function _renderCompositeBtn() {
|
||||||
var btn = $('#planModeBtn');
|
var btn = $('#planModeBtn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -281,17 +289,19 @@ async function cycleCompositeMode() {
|
|||||||
_planModeActive = (_compositeMode === 'plan');
|
_planModeActive = (_compositeMode === 'plan');
|
||||||
_renderCompositeBtn();
|
_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') {
|
if (_compositeMode !== 'plan') {
|
||||||
var sid = getCurrentSessionId();
|
var sid = getCurrentSessionId();
|
||||||
if (sid) {
|
if (sid) {
|
||||||
try {
|
var targetMode = _compositeMode;
|
||||||
await apiUpdateSession(sid, { multiModelMode: _compositeMode });
|
_modePersist = _modePersist
|
||||||
} catch (e) {
|
.then(function () { return apiUpdateSession(sid, { multiModelMode: targetMode }); })
|
||||||
console.error('Modus speichern fehlgeschlagen:', e);
|
.catch(function (e) {
|
||||||
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
|
console.error('Modus speichern fehlgeschlagen:', e);
|
||||||
return;
|
if (typeof showToast === 'function') showToast('Modus konnte nicht gespeichert werden', 'error');
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +590,11 @@ async function sendMessage() {
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
input.dispatchEvent(new Event('input'));
|
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));
|
await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +631,7 @@ async function sendMessageDirect(text) {
|
|||||||
if (isStreaming || !text) return;
|
if (isStreaming || !text) return;
|
||||||
addUserBubble(text, true);
|
addUserBubble(text, true);
|
||||||
scrollDownForce();
|
scrollDownForce();
|
||||||
|
await _modePersist; // offenes Modus-PATCH abwarten (s. sendMessage)
|
||||||
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
|
await _runStream((signal) => apiChat(text, getCurrentSessionId(), signal, _planModeActive));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user