fix(workboard): skip read-only lifecycle writes

This commit is contained in:
Peter Steinberger
2026-05-22 22:06:43 +01:00
parent e961803332
commit 83f006a11d
5 changed files with 42 additions and 2 deletions

View File

@@ -22,7 +22,7 @@ import {
dismissChatError,
switchChatSession,
} from "./app-render.helpers.ts";
import { warnQueryToken } from "./app-settings.ts";
import { hasOperatorWriteAccess, warnQueryToken } from "./app-settings.ts";
import type { AppViewState } from "./app-view-state.ts";
import { reconcileChatRunLifecycle } from "./chat/run-lifecycle.ts";
import {
@@ -2203,6 +2203,10 @@ export function renderApp(state: AppViewState) {
host: state,
client: state.client,
connected: state.connected,
canWrite: hasOperatorWriteAccess(
(state.hello as { auth?: { role?: string; scopes?: string[] } } | null)?.auth ??
null,
),
pluginEnabled: isPluginEnabledInConfigSnapshot(state.configSnapshot, "workboard", {
enabledByDefault: false,
}),

View File

@@ -795,6 +795,19 @@ export function hasOperatorReadAccess(
});
}
export function hasOperatorWriteAccess(
auth: { role?: string; scopes?: readonly string[] } | null,
): boolean {
if (!auth?.scopes) {
return true;
}
return roleScopesAllow({
role: auth.role ?? "operator",
requestedScopes: ["operator.write"],
allowedScopes: auth.scopes,
});
}
export function hasMissingSkillDependencies(
missing: Record<string, unknown> | null | undefined,
): boolean {

View File

@@ -458,6 +458,26 @@ describe("workboard controller", () => {
expect(state.cards.find((card) => card.id === "card-review")?.status).toBe("review");
});
it("skips lifecycle writeback for read-only workboard clients", async () => {
const host = {};
const state = getWorkboardState(host);
state.loaded = true;
state.cards = [{ ...sampleCard, sessionKey: sampleSession.key }];
const client = createClient(() => {
throw new Error("write denied");
});
await syncWorkboardLifecycle({
host,
client: client as never,
sessions: [sampleSession],
canWrite: false,
});
expect(client.request).not.toHaveBeenCalled();
expect(state.error).toBeNull();
});
it("resyncs cards manually moved back to an active lifecycle column", async () => {
const host = {};
const state = getWorkboardState(host);

View File

@@ -536,10 +536,11 @@ export async function syncWorkboardLifecycle(params: {
host: WorkboardHost;
client: GatewayBrowserClient | null;
sessions: readonly GatewaySessionRow[];
canWrite?: boolean;
requestUpdate?: () => void;
}) {
const state = getWorkboardState(params.host);
if (!params.client || !state.loaded) {
if (!params.client || !state.loaded || params.canWrite === false) {
return;
}
const syncKeys = getLifecycleSyncKeys(params.host);

View File

@@ -26,6 +26,7 @@ type WorkboardProps = {
host: object;
client: GatewayBrowserClient | null;
connected: boolean;
canWrite?: boolean;
pluginEnabled: boolean;
agentsList: AgentsListResult | null;
sessions: GatewaySessionRow[];
@@ -729,6 +730,7 @@ export function renderWorkboard(props: WorkboardProps) {
host: props.host,
client: props.client,
sessions: props.sessions,
canWrite: props.canWrite,
requestUpdate: props.onRequestUpdate,
});
}