mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 18:14:06 +00:00
fix(workboard): skip read-only lifecycle writes
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user