openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
786 lines (785 loc) • 38.5 kB
JavaScript
import { l as runSqliteDeferredTransactionSync, u as runSqliteImmediateTransactionSync } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { J as ensureOpenClawAgentBoardSchemaInTransaction } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import { g as runOpenClawAgentWriteTransaction, p as openOpenClawAgentDatabase } from "./openclaw-agent-db-CWtDoRbC.js";
import { c as applyBoardOps, l as insertBoardWidget, o as BOARD_SIZE_PRESETS, r as GITHUB_ACTIONS_GRANT_PREFIX, s as BoardValidationError, u as normalizeBoardLayout } from "./github-actions-capability-rN2d0K3V.js";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-CHjf8FxN.js";
import { r as normalizeBoardWidgetDeclared, t as boardDeclarationIsSubset } from "./board-capabilities-2QIFZaOL.js";
import { createHash, randomBytes } from "node:crypto";
//#region src/boards/board-store.ts
const BOARD_MAX_WIDGETS = 48;
const BOARD_MAX_WIDGET_HTML_BYTES = 262144;
const BOARD_MAX_WIDGET_PLUGIN_PROPS_BYTES = 8192;
function cloneBoardSnapshot(snapshot) {
return {
sessionKey: snapshot.sessionKey,
revision: snapshot.revision,
tabs: snapshot.tabs.map((tab) => ({ ...tab })),
widgets: snapshot.widgets.map((widget) => ({
...widget,
...widget.props !== void 0 ? { props: structuredClone(widget.props) } : {},
...widget.declaredSummary !== void 0 ? { declaredSummary: [...widget.declaredSummary] } : {},
...widget.declared !== void 0 ? { declared: {
...widget.declared.netOrigins ? { netOrigins: [...widget.declared.netOrigins] } : {},
...widget.declared.tools ? { tools: [...widget.declared.tools] } : {}
} } : {}
}))
};
}
function createBoardDeclaredSummary(declared) {
const lines = [...(declared?.netOrigins ?? []).map((origin) => `Network access: ${origin}`), ...(declared?.tools ?? []).map((tool) => tool.startsWith(GITHUB_ACTIONS_GRANT_PREFIX) ? `GitHub Actions metadata: ${tool.slice(GITHUB_ACTIONS_GRANT_PREFIX.length)} (including private repository data accessible to the agent; shared with this widget/session audience)` : `Tool access: ${tool}`)];
return lines.length > 0 ? lines : void 0;
}
function generatedIdentityMatches(left, right) {
return left?.kind === "generated" && left.source === right.source && left.key === right.key;
}
function resolveBoardWidgetPutParams(prior, params, nameIdentities) {
const generatedIdentity = params.generatedIdentity;
if (!generatedIdentity) return params;
if (generatedIdentity.fallbackName === params.name) throw new BoardValidationError("invalid_operation", "generated widget fallback name must differ from its preferred name");
const marker = {
kind: "generated",
source: generatedIdentity.source,
key: generatedIdentity.key
};
const existingGenerated = prior.widgets.find((widget) => generatedIdentityMatches(nameIdentities.get(widget.name), marker));
if (existingGenerated) return {
...params,
name: existingGenerated.name
};
if (!prior.widgets.find((widget) => widget.name === params.name)) return params;
if (prior.widgets.find((widget) => widget.name === generatedIdentity.fallbackName)) throw new BoardValidationError("conflict", `generated widget fallback name is already in use: ${generatedIdentity.fallbackName}`);
return {
...params,
name: generatedIdentity.fallbackName
};
}
function normalizeBoardWidgetPutParams(params, sessionKey = params.sessionKey) {
const declared = normalizeBoardWidgetDeclared(params.declared);
const canonical = {
...params,
sessionKey
};
if (declared) canonical.declared = declared;
else delete canonical.declared;
return canonical;
}
function createBoardWidgetPutResult(snapshot, resolvedWidgetName) {
return {
...cloneBoardSnapshot(snapshot),
resolvedWidgetName
};
}
function validatePluginContent(params) {
if (params.content.kind !== "plugin") return;
if (params.declared !== void 0) throw new BoardValidationError("invalid_operation", "trusted plugin widgets do not accept sandbox capability declarations");
if (Buffer.byteLength(JSON.stringify(params.content.props ?? {}), "utf8") > BOARD_MAX_WIDGET_PLUGIN_PROPS_BYTES) throw new BoardValidationError("invalid_operation", `board plugin widget props exceed ${BOARD_MAX_WIDGET_PLUGIN_PROPS_BYTES} UTF-8 bytes`);
}
function validateRegisteredContent(params) {
if (params.content.kind !== "registered") return;
if (Buffer.byteLength(params.content.source, "utf8") > BOARD_MAX_WIDGET_HTML_BYTES) throw new BoardValidationError("invalid_operation", `board registered widget source exceeds ${BOARD_MAX_WIDGET_HTML_BYTES} UTF-8 bytes`);
}
function createBoardWidgetPutSnapshot(prior, params, context) {
validatePluginContent(params);
validateRegisteredContent(params);
if (params.content.kind === "html" && Buffer.byteLength(params.content.html, "utf8") > BOARD_MAX_WIDGET_HTML_BYTES) throw new BoardValidationError("invalid_operation", `board widget HTML exceeds ${BOARD_MAX_WIDGET_HTML_BYTES} UTF-8 bytes`);
let layout = normalizeBoardLayout(prior);
if (layout.tabs.length === 0) layout.tabs.push({
tabId: "main",
title: "Main",
position: 0,
chatDock: "right"
});
const existing = layout.widgets.find((widget) => widget.name === params.name);
if (existing && (existing.contentOwner !== params.content.kind || (params.content.kind === "plugin" || params.content.kind === "registered") && (existing.pluginKind !== params.content.pluginKind || params.content.kind === "registered" && existing.registeredContentKind !== params.content.contentKind))) {
const incomingOwner = params.content.kind === "plugin" || params.content.kind === "registered" ? params.content.pluginKind : params.content.kind;
throw new BoardValidationError("invalid_operation", `board widget ${params.name} contains ${existing.pluginKind ?? existing.contentOwner} content; update it with the same content kind or remove it before replacing it with ${incomingOwner} content`);
}
if (!existing && layout.widgets.length >= BOARD_MAX_WIDGETS) throw new BoardValidationError("invalid_operation", `board cannot contain more than ${BOARD_MAX_WIDGETS} widgets`);
const tabId = params.placement?.tabId ?? existing?.tabId ?? layout.tabs[0].tabId;
if (!layout.tabs.some((tab) => tab.tabId === tabId)) throw new BoardValidationError("not_found", `board tab not found: ${tabId}`);
const size = BOARD_SIZE_PRESETS[params.placement?.size ?? "md"];
const widgetRevision = (existing?.revision ?? 0) + 1;
const declared = params.content.kind === "plugin" ? void 0 : normalizeBoardWidgetDeclared(params.declared);
const declaredSummary = createBoardDeclaredSummary(declared);
const contentSha256 = params.content.kind === "html" ? createHash("sha256").update(params.content.html).digest("hex") : params.content.kind === "registered" ? createHash("sha256").update(params.content.source).digest("hex") : void 0;
const preservesGrant = declared !== void 0 && context.grantScopeMatches && (params.content.kind !== "mcp-app" || params.content.interactive) && existing?.grantState === "granted" && (params.content.kind === "html" || params.content.kind === "registered" ? contentSha256 === context.grantedSha256 : true) && boardDeclarationIsSubset(declared, existing.declared);
layout = insertBoardWidget(layout, {
name: params.name,
tabId,
...params.title !== void 0 ? { title: params.title } : existing?.title !== void 0 ? { title: existing.title } : {},
contentKind: params.content.kind === "registered" ? "plugin" : params.content.kind,
contentOwner: params.content.kind,
...params.content.kind === "registered" ? { registeredContentKind: params.content.contentKind } : {},
...params.presentation !== void 0 ? { presentation: params.presentation } : existing?.presentation !== void 0 ? { presentation: existing.presentation } : {},
...params.heightMode !== void 0 ? { heightMode: params.heightMode } : existing?.heightMode !== void 0 ? { heightMode: existing.heightMode } : {},
...params.content.kind === "plugin" || params.content.kind === "registered" ? {
pluginKind: params.content.pluginKind,
...params.content.kind === "plugin" && params.content.props !== void 0 ? { props: structuredClone(params.content.props) } : {}
} : {},
sizeW: params.placement?.size ? size.sizeW : existing?.sizeW ?? size.sizeW,
sizeH: params.placement?.size ? size.sizeH : existing?.sizeH ?? size.sizeH,
position: existing?.position ?? layout.widgets.length,
grantState: params.content.kind === "plugin" ? "none" : preservesGrant ? "granted" : params.content.kind === "mcp-app" && !params.content.interactive ? "none" : declaredSummary || params.content.kind === "mcp-app" ? "pending" : "none",
revision: widgetRevision,
...params.content.kind !== "plugin" ? { instanceId: context.instanceId } : {},
...declaredSummary ? { declaredSummary } : {},
...declared ? { declared } : {}
}, {
tabId,
...params.placement?.after ? { after: params.placement.after } : {},
move: params.placement?.tabId !== void 0 || params.placement?.after !== void 0
});
return {
sessionKey: params.sessionKey,
revision: prior.revision + 1,
...layout
};
}
function createBoardGrantSnapshot(current, name, decision, revision, instanceId) {
const widget = current.widgets.find((candidate) => candidate.name === name);
if (!widget) throw new BoardValidationError("not_found", `board widget not found: ${name}`);
if (widget.revision !== revision) throw new BoardValidationError("conflict", `board widget revision changed: ${name} is revision ${widget.revision}, not ${revision}`);
if (widget.instanceId !== void 0 && widget.instanceId !== instanceId) throw new BoardValidationError("conflict", `board widget instance changed: ${name}`);
if (widget.grantState !== "pending") throw new BoardValidationError("invalid_operation", `board widget grant is not pending: ${name}`);
const snapshot = cloneBoardSnapshot(current);
snapshot.widgets.find((candidate) => candidate.name === name).grantState = decision;
snapshot.revision += 1;
return snapshot;
}
//#endregion
//#region src/boards/sqlite-board-codec.ts
const BOARD_WIDGET_SNAPSHOT_COLUMNS = [
"session_key",
"name",
"tab_id",
"title",
"content_kind",
"descriptor_json",
"sha256",
"view_generation",
"revision",
"size_w",
"size_h",
"position",
"manifest",
"grant_state",
"granted_sha",
"created_by",
"created_at",
"updated_at"
];
const BOARD_GRANT_SEMANTICS_VERSION = 2;
function parseManifest(value) {
const parsed = JSON.parse(value);
const contentOwnerPresent = Object.hasOwn(parsed, "contentOwner");
const contentOwner = parsed.contentOwner === "html" || parsed.contentOwner === "mcp-app" || parsed.contentOwner === "plugin" || parsed.contentOwner === "registered" ? parsed.contentOwner : void 0;
const registeredContentKind = typeof parsed.registeredContentKind === "string" && /^[a-z][a-z0-9-]{0,31}$/u.test(parsed.registeredContentKind) ? parsed.registeredContentKind : void 0;
if (contentOwnerPresent && contentOwner === void 0 || contentOwner === "registered" && registeredContentKind === void 0 || contentOwner !== "registered" && Object.hasOwn(parsed, "registeredContentKind")) throw new BoardValidationError("invalid_operation", "board widget content ownership is invalid");
const ownership = {
...contentOwner ? { contentOwner } : {},
...registeredContentKind ? { registeredContentKind } : {}
};
const netOrigins = Array.isArray(parsed.netOrigins) ? parsed.netOrigins.filter((entry) => typeof entry === "string") : void 0;
const tools = Array.isArray(parsed.tools) ? parsed.tools.filter((entry) => typeof entry === "string") : void 0;
const mcpAppInteractive = typeof parsed.mcpAppInteractive === "boolean" ? parsed.mcpAppInteractive : void 0;
const mcpAppInstanceId = typeof parsed.mcpAppInstanceId === "string" && /^[a-f0-9]{32}$/u.test(parsed.mcpAppInstanceId) ? parsed.mcpAppInstanceId : void 0;
const registeredInstanceId = typeof parsed.registeredInstanceId === "string" && /^[a-f0-9]{32}$/u.test(parsed.registeredInstanceId) ? parsed.registeredInstanceId : void 0;
const presentation = parsed.presentation === "card" || parsed.presentation === "full-bleed" || parsed.presentation === "frameless" ? parsed.presentation : void 0;
const heightMode = parsed.heightMode === "auto" || parsed.heightMode === "fixed" ? parsed.heightMode : void 0;
const nameIdentityPresent = Object.hasOwn(parsed, "nameIdentity");
const nameIdentity = (() => {
if (!parsed.nameIdentity || typeof parsed.nameIdentity !== "object" || Array.isArray(parsed.nameIdentity)) return;
const identity = parsed.nameIdentity;
if (identity.kind === "explicit") return { kind: "explicit" };
if (identity.kind === "generated" && identity.source === "show_widget" && typeof identity.key === "string" && /^[a-f0-9]{64}$/u.test(identity.key)) return {
kind: "generated",
source: "show_widget",
key: identity.key
};
})();
try {
const declared = normalizeBoardWidgetDeclared({
...netOrigins?.length ? { netOrigins } : {},
...tools?.length ? { tools } : {}
});
return {
...ownership,
...declared ? { declared } : {},
...parsed.grantSemanticsVersion === BOARD_GRANT_SEMANTICS_VERSION ? { grantSemanticsVersion: BOARD_GRANT_SEMANTICS_VERSION } : {},
...presentation ? { presentation } : {},
...heightMode ? { heightMode } : {},
...nameIdentity ? { nameIdentity } : {},
...!nameIdentity && nameIdentityPresent ? { nameIdentityInvalid: true } : {},
...mcpAppInteractive !== void 0 ? { mcpAppInteractive } : {},
...mcpAppInstanceId ? { mcpAppInstanceId } : {},
...registeredInstanceId ? { registeredInstanceId } : {}
};
} catch (error) {
if (error instanceof BoardValidationError) return {
...ownership,
declarationInvalid: true
};
throw error;
}
}
function serializeManifest(ownership, declared, grantState, frameAuthority, widgetOptions, nameIdentity) {
return JSON.stringify({
...ownership,
...declared,
...widgetOptions?.presentation ? { presentation: widgetOptions.presentation } : {},
...widgetOptions?.heightMode ? { heightMode: widgetOptions.heightMode } : {},
...nameIdentity ? { nameIdentity } : {},
...grantState === "granted" ? { grantSemanticsVersion: BOARD_GRANT_SEMANTICS_VERSION } : {},
...frameAuthority?.kind === "mcp-app" ? {
mcpAppInteractive: frameAuthority.interactive,
mcpAppInstanceId: frameAuthority.instanceId
} : {},
...frameAuthority?.kind === "registered" ? { registeredInstanceId: frameAuthority.instanceId } : {}
});
}
function createBoardWidgetContentFields(params, frame, revision, grantState, viewGeneration, now) {
const manifest = serializeManifest({
contentOwner: params.content.kind,
...params.content.kind === "registered" ? { registeredContentKind: params.content.contentKind } : {}
}, params.declared, grantState, params.content.kind === "mcp-app" ? {
kind: "mcp-app",
interactive: params.content.interactive,
instanceId: viewGeneration
} : params.content.kind === "registered" ? {
kind: "registered",
instanceId: viewGeneration
} : void 0, frame, params.generatedIdentity ? {
kind: "generated",
source: params.generatedIdentity.source,
key: params.generatedIdentity.key
} : { kind: "explicit" });
if (params.content.kind === "html") {
const sha256 = createHash("sha256").update(params.content.html).digest("hex");
return {
content_kind: "html",
html: Buffer.from(params.content.html, "utf8"),
descriptor_json: null,
sha256,
view_generation: viewGeneration,
revision,
manifest,
grant_state: grantState,
granted_sha: grantState === "granted" ? sha256 : null,
updated_at: now
};
}
if (params.content.kind === "plugin") {
const descriptorJson = JSON.stringify({
pluginKind: params.content.pluginKind,
...params.content.props !== void 0 ? { props: params.content.props } : {}
});
return {
content_kind: "plugin",
html: null,
descriptor_json: descriptorJson,
sha256: createHash("sha256").update(descriptorJson).digest("hex"),
view_generation: null,
revision,
manifest,
grant_state: "none",
granted_sha: null,
updated_at: now
};
}
if (params.content.kind === "registered") {
const descriptorJson = JSON.stringify({
pluginKind: params.content.pluginKind,
source: params.content.source
});
const sha256 = createHash("sha256").update(params.content.source).digest("hex");
return {
content_kind: "plugin",
html: null,
descriptor_json: descriptorJson,
sha256,
view_generation: null,
revision,
manifest,
grant_state: grantState,
granted_sha: grantState === "granted" ? sha256 : null,
updated_at: now
};
}
const descriptorJson = JSON.stringify(params.content.descriptor);
const sha256 = createHash("sha256").update(descriptorJson).digest("hex");
return {
content_kind: "mcp-app",
html: null,
descriptor_json: descriptorJson,
sha256,
view_generation: null,
revision,
manifest,
grant_state: grantState,
granted_sha: grantState === "granted" ? sha256 : null,
updated_at: now
};
}
function resolveSqliteBoardWidgetPutParams(snapshot, params, rows) {
const identities = /* @__PURE__ */ new Map();
for (const row of rows) {
const manifest = parseManifest(row.manifest);
const nameIdentity = manifest.nameIdentity ?? (manifest.nameIdentityInvalid ? { kind: "invalid" } : void 0);
if (nameIdentity) identities.set(row.name, nameIdentity);
}
return resolveBoardWidgetPutParams(snapshot, params, identities);
}
function updateManifestHeightMode(value, heightMode) {
const parsed = JSON.parse(value);
return JSON.stringify({
...parsed,
heightMode
});
}
function effectiveGrantState(storedGrantState, manifest) {
const grantState = storedGrantState === "pending" || storedGrantState === "granted" || storedGrantState === "rejected" ? storedGrantState : "none";
if (manifest.declarationInvalid || !manifest.declared && manifest.mcpAppInteractive !== true) return grantState === "rejected" ? "rejected" : "none";
if (grantState === "granted" && manifest.grantSemanticsVersion !== BOARD_GRANT_SEMANTICS_VERSION) return "pending";
return grantState;
}
function parseDescriptor(value) {
return JSON.parse(value);
}
function parsePluginContent(value) {
const parsed = JSON.parse(value);
if (typeof parsed.pluginKind !== "string") throw new BoardValidationError("invalid_operation", "board plugin widget kind is invalid");
return typeof parsed.source === "string" ? {
pluginKind: parsed.pluginKind,
source: parsed.source
} : {
pluginKind: parsed.pluginKind,
...parsed.props !== void 0 ? { props: parsed.props } : {}
};
}
function rowToRegisteredDocument(row) {
if (row.content_kind !== "plugin" || row.descriptor_json === null) return;
const content = parsePluginContent(row.descriptor_json);
const manifest = parseManifest(row.manifest);
if (!("source" in content) || !manifest.registeredInstanceId) return;
return {
pluginKind: content.pluginKind,
source: content.source,
...row.title !== null ? { title: row.title } : {},
revision: row.revision,
sha256: row.sha256,
viewGeneration: manifest.registeredInstanceId,
grantState: effectiveGrantState(row.grant_state, manifest),
...manifest.declared ? { declared: manifest.declared } : {}
};
}
function rowToTab(row) {
return {
tabId: row.tab_id,
title: row.title,
position: row.position,
chatDock: row.chat_dock
};
}
function rowToWidget(row, manifest = parseManifest(row.manifest)) {
const declared = manifest.declared;
const declaredSummary = createBoardDeclaredSummary(declared);
const pluginContent = row.content_kind === "plugin" && row.descriptor_json !== null ? parsePluginContent(row.descriptor_json) : void 0;
const persistedOwner = pluginContent && "source" in pluginContent ? "registered" : row.content_kind === "html" || row.content_kind === "mcp-app" || row.content_kind === "plugin" ? row.content_kind : void 0;
if (persistedOwner === void 0) throw new BoardValidationError("invalid_operation", "board widget content owner is invalid");
const persistedRegisteredContentKind = persistedOwner === "registered" ? pluginContent.pluginKind.match(/^[a-z0-9][a-z0-9-]{0,63}:([a-z][a-z0-9-]{0,31})$/u)?.[1] : void 0;
if (manifest.contentOwner !== void 0 && manifest.contentOwner !== persistedOwner || persistedOwner === "registered" && (persistedRegisteredContentKind === void 0 || manifest.registeredContentKind !== void 0 && manifest.registeredContentKind !== persistedRegisteredContentKind)) throw new BoardValidationError("invalid_operation", "board widget content ownership does not match persisted content");
const contentOwner = manifest.contentOwner ?? persistedOwner;
const registeredContentKind = manifest.registeredContentKind ?? persistedRegisteredContentKind;
const instanceId = row.content_kind === "mcp-app" ? manifest.mcpAppInstanceId : pluginContent && "source" in pluginContent ? manifest.registeredInstanceId : row.view_generation;
return {
name: row.name,
tabId: row.tab_id,
...row.title !== null ? { title: row.title } : {},
contentKind: row.content_kind,
contentOwner,
...registeredContentKind ? { registeredContentKind } : {},
...manifest.presentation ? { presentation: manifest.presentation } : {},
...manifest.heightMode ? { heightMode: manifest.heightMode } : {},
...pluginContent ? {
pluginKind: pluginContent.pluginKind,
..."props" in pluginContent && pluginContent.props !== void 0 ? { props: pluginContent.props } : {}
} : {},
sizeW: row.size_w,
sizeH: row.size_h,
position: row.position,
grantState: effectiveGrantState(row.grant_state, manifest),
revision: row.revision,
...instanceId ? { instanceId } : {},
...declaredSummary ? { declaredSummary } : {},
...declared ? { declared } : {}
};
}
function rowToHtmlViewMetadata(row, manifest) {
const viewGeneration = row.content_kind === "html" ? row.view_generation : manifest.registeredInstanceId;
if (viewGeneration === null || viewGeneration === void 0) return;
const declared = manifest.declared;
return {
revision: row.revision,
sha256: row.sha256,
viewGeneration,
grantState: effectiveGrantState(row.grant_state, manifest),
...declared ? { declared } : {}
};
}
//#endregion
//#region src/boards/sqlite-board-store.ts
const ensuredBoardDatabases = /* @__PURE__ */ new WeakSet();
const presentBoardDatabases = /* @__PURE__ */ new WeakSet();
function boardTablesPresent(database) {
if (ensuredBoardDatabases.has(database.db) || presentBoardDatabases.has(database.db)) return true;
if (!database.db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'board_widgets'").get()) return false;
presentBoardDatabases.add(database.db);
return true;
}
function listBoardSessionKeysReadOnly(params) {
const result = withOpenClawAgentDatabaseReadOnly((database) => {
if (!boardTablesPresent(database)) return [];
const db = getNodeSqliteKysely(database.db);
return executeSqliteQuerySync(database.db, db.selectFrom("board_tabs").select("session_key").distinct()).rows.map((row) => row.session_key);
}, params);
return new Set(result.found ? result.value : []);
}
function ensureBoardSchema(database) {
if (ensuredBoardDatabases.has(database.db)) return;
if (database.db.isTransaction) throw new Error("board schema must be ensured before the write transaction starts");
runSqliteImmediateTransactionSync(database.db, () => ensureOpenClawAgentBoardSchemaInTransaction(database.db), {
databaseLabel: database.path,
operationLabel: "board.ensure-schema"
});
ensuredBoardDatabases.add(database.db);
presentBoardDatabases.add(database.db);
}
function readStoredBoard(database, sessionKey) {
return runSqliteDeferredTransactionSync(database.db, () => {
const db = getNodeSqliteKysely(database.db);
const tabRows = executeSqliteQuerySync(database.db, db.selectFrom("board_tabs").selectAll().where("session_key", "=", sessionKey).orderBy("position", "asc").orderBy("tab_id", "asc")).rows;
const admittedWidgetRows = executeSqliteQuerySync(database.db, db.selectFrom("board_widgets").select(BOARD_WIDGET_SNAPSHOT_COLUMNS).where("session_key", "=", sessionKey).orderBy("tab_id", "asc").orderBy("position", "asc").orderBy("name", "asc")).rows.map((row) => ({
row,
manifest: parseManifest(row.manifest)
})).filter(({ row, manifest }) => {
if (row.content_kind !== "mcp-app") return true;
return manifest.mcpAppInteractive !== void 0 && manifest.mcpAppInstanceId !== void 0;
});
const htmlViewMetadata = /* @__PURE__ */ new Map();
for (const { row, manifest } of admittedWidgetRows) {
const metadata = rowToHtmlViewMetadata(row, manifest);
if (metadata) htmlViewMetadata.set(row.name, metadata);
}
const layout = normalizeBoardLayout({
tabs: tabRows.map(rowToTab),
widgets: admittedWidgetRows.map(({ row, manifest }) => rowToWidget(row, manifest))
});
return {
snapshot: {
sessionKey,
revision: tabRows.reduce((revision, row) => Math.max(revision, row.revision), 0),
...layout
},
tabRows,
widgetRows: admittedWidgetRows.map(({ row }) => row),
htmlViewMetadata
};
}, {
databaseLabel: database.path,
operationLabel: "board.read"
});
}
function rowToHtmlDocument(row) {
if (row.content_kind !== "html" || row.html === null || row.view_generation === null) return;
const manifest = parseManifest(row.manifest);
const declared = manifest.declared;
return {
html: Buffer.from(row.html).toString("utf8"),
revision: row.revision,
sha256: row.sha256,
viewGeneration: row.view_generation,
grantState: effectiveGrantState(row.grant_state, manifest),
...declared ? { declared } : {}
};
}
function upsertTabs(database, previous, next) {
const db = getNodeSqliteKysely(database.db);
const createdBy = new Map(previous.tabRows.map((row) => [row.tab_id, row.created_by]));
for (const tab of next.tabs) executeSqliteQuerySync(database.db, db.insertInto("board_tabs").values({
session_key: next.sessionKey,
tab_id: tab.tabId,
title: tab.title,
position: tab.position,
chat_dock: tab.chatDock,
created_by: createdBy.get(tab.tabId) ?? "agent",
revision: next.revision
}).onConflict((conflict) => conflict.columns(["session_key", "tab_id"]).doUpdateSet({
title: tab.title,
position: tab.position,
chat_dock: tab.chatDock,
revision: next.revision
})));
}
function updateWidgetLayouts(database, snapshot, updatedAt) {
const db = getNodeSqliteKysely(database.db);
for (const widget of snapshot.widgets) executeSqliteQuerySync(database.db, db.updateTable("board_widgets").set({
tab_id: widget.tabId,
title: widget.title ?? null,
size_w: widget.sizeW,
size_h: widget.sizeH,
position: widget.position,
updated_at: updatedAt
}).where("session_key", "=", snapshot.sessionKey).where("name", "=", widget.name));
}
function updateWidgetHeightModes(database, previous, ops) {
const db = getNodeSqliteKysely(database.db);
for (const op of ops) {
if (op.kind !== "widget_resize") continue;
const row = previous.widgetRows.find((candidate) => candidate.name === op.name);
if (!row) continue;
executeSqliteQuerySync(database.db, db.updateTable("board_widgets").set({ manifest: updateManifestHeightMode(row.manifest, op.heightMode ?? "fixed") }).where("session_key", "=", previous.snapshot.sessionKey).where("name", "=", op.name));
}
}
function deleteRemovedWidgets(database, previous, next) {
const db = getNodeSqliteKysely(database.db);
const widgetNames = new Set(next.widgets.map((widget) => widget.name));
for (const row of previous.widgetRows) if (!widgetNames.has(row.name)) executeSqliteQuerySync(database.db, db.deleteFrom("board_widgets").where("session_key", "=", next.sessionKey).where("name", "=", row.name));
}
function deleteRemovedTabs(database, previous, next) {
const db = getNodeSqliteKysely(database.db);
const tabIds = new Set(next.tabs.map((tab) => tab.tabId));
for (const row of previous.tabRows) if (!tabIds.has(row.tab_id)) executeSqliteQuerySync(database.db, db.deleteFrom("board_tabs").where("session_key", "=", next.sessionKey).where("tab_id", "=", row.tab_id));
}
function hasSession(database, sessionKey) {
const db = getNodeSqliteKysely(database.db);
const row = executeSqliteQuerySync(database.db, db.selectFrom("session_nodes").select("entry_json").where("session_key", "=", sessionKey).limit(1)).rows[0];
if (!row) return false;
try {
const entry = JSON.parse(row.entry_json);
return Boolean(entry && typeof entry === "object" && !Array.isArray(entry) && typeof entry.sessionId === "string");
} catch {
return false;
}
}
function emptyBoardSnapshot(sessionKey) {
return {
sessionKey,
revision: 0,
tabs: [],
widgets: []
};
}
var SqliteBoardStore = class {
constructor(options) {
this.options = options;
}
resolve(target) {
return this.options.resolveSession(target);
}
requireExistingSession(resolved) {
const result = withOpenClawAgentDatabaseReadOnly((database) => hasSession(database, resolved.sessionKey), {
agentId: resolved.agentId,
...resolved.path ? { path: resolved.path } : {},
env: this.options.env
});
if (!result.found || !result.value) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`);
}
prepareWrite(target) {
const resolved = this.resolve(target);
this.requireExistingSession(resolved);
const database = openOpenClawAgentDatabase({
agentId: resolved.agentId,
...resolved.path ? { path: resolved.path } : {},
env: this.options.env
});
ensureBoardSchema(database);
return {
database,
resolved
};
}
getSnapshot(target) {
const resolved = this.resolve(target);
const result = withOpenClawAgentDatabaseReadOnly((database) => hasSession(database, resolved.sessionKey) && boardTablesPresent(database) ? readStoredBoard(database, resolved.sessionKey).snapshot : void 0, {
agentId: resolved.agentId,
...resolved.path ? { path: resolved.path } : {},
env: this.options.env
});
return cloneBoardSnapshot(result.found && result.value ? result.value : emptyBoardSnapshot(resolved.sessionKey));
}
getSnapshotWithHtmlViewMetadata(target) {
const resolved = this.resolve(target);
const result = withOpenClawAgentDatabaseReadOnly((database) => hasSession(database, resolved.sessionKey) && boardTablesPresent(database) ? readStoredBoard(database, resolved.sessionKey) : void 0, {
agentId: resolved.agentId,
...resolved.path ? { path: resolved.path } : {},
env: this.options.env
});
const stored = result.found ? result.value : void 0;
return {
snapshot: cloneBoardSnapshot(stored?.snapshot ?? emptyBoardSnapshot(resolved.sessionKey)),
htmlViewMetadata: stored?.htmlViewMetadata ?? /* @__PURE__ */ new Map()
};
}
applyOps(target, ops) {
if (ops.length === 0) return this.getSnapshot(target);
const { database, resolved } = this.prepareWrite(target);
return runOpenClawAgentWriteTransaction((transactionDatabase) => {
if (!hasSession(transactionDatabase, resolved.sessionKey)) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`);
const previous = readStoredBoard(transactionDatabase, resolved.sessionKey);
const layout = applyBoardOps(previous.snapshot, ops);
const next = {
sessionKey: resolved.sessionKey,
revision: previous.snapshot.revision + 1,
...layout
};
const now = Date.now();
upsertTabs(transactionDatabase, previous, next);
deleteRemovedWidgets(transactionDatabase, previous, next);
updateWidgetLayouts(transactionDatabase, next, now);
updateWidgetHeightModes(transactionDatabase, previous, ops);
deleteRemovedTabs(transactionDatabase, previous, next);
return cloneBoardSnapshot(next);
}, {
agentId: resolved.agentId,
path: database.path,
env: this.options.env
}, { operationLabel: "board.apply-ops" });
}
putWidget(params) {
const { database, resolved } = this.prepareWrite(params);
const canonicalInput = normalizeBoardWidgetPutParams(params, resolved.sessionKey);
const viewGeneration = randomBytes(16).toString("hex");
return runOpenClawAgentWriteTransaction((transactionDatabase) => {
if (!hasSession(transactionDatabase, resolved.sessionKey)) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`);
const previous = readStoredBoard(transactionDatabase, resolved.sessionKey);
const canonicalParams = resolveSqliteBoardWidgetPutParams(previous.snapshot, canonicalInput, previous.widgetRows);
const existing = previous.widgetRows.find((row) => row.name === canonicalParams.name);
const grantScopeMatches = existing ? existing.content_kind === "html" ? canonicalParams.content.kind === "html" : existing.content_kind === "mcp-app" ? existing.descriptor_json !== null && canonicalParams.content.kind === "mcp-app" && parseDescriptor(existing.descriptor_json).serverName === canonicalParams.content.descriptor.serverName : existing.descriptor_json !== null && (canonicalParams.content.kind === "plugin" || canonicalParams.content.kind === "registered") && parsePluginContent(existing.descriptor_json).pluginKind === canonicalParams.content.pluginKind : true;
const next = createBoardWidgetPutSnapshot(previous.snapshot, canonicalParams, {
grantScopeMatches,
grantedSha256: existing?.granted_sha ?? void 0,
instanceId: viewGeneration
});
const widget = next.widgets.find((candidate) => candidate.name === canonicalParams.name);
const now = Date.now();
upsertTabs(transactionDatabase, previous, next);
const db = getNodeSqliteKysely(transactionDatabase.db);
const fields = createBoardWidgetContentFields(canonicalParams, {
presentation: widget.presentation,
heightMode: widget.heightMode
}, widget.revision, widget.grantState, viewGeneration, now);
executeSqliteQuerySync(transactionDatabase.db, db.insertInto("board_widgets").values({
session_key: resolved.sessionKey,
name: canonicalParams.name,
tab_id: widget.tabId,
title: widget.title ?? null,
size_w: widget.sizeW,
size_h: widget.sizeH,
position: widget.position,
created_by: existing?.created_by ?? "agent",
created_at: existing?.created_at ?? now,
...fields
}).onConflict((conflict) => conflict.columns(["session_key", "name"]).doUpdateSet({
tab_id: widget.tabId,
title: widget.title ?? null,
size_w: widget.sizeW,
size_h: widget.sizeH,
position: widget.position,
...fields
})));
updateWidgetLayouts(transactionDatabase, next, now);
return createBoardWidgetPutResult(next, canonicalParams.name);
}, {
agentId: resolved.agentId,
path: database.path,
env: this.options.env
}, { operationLabel: "board.put-widget" });
}
grant(target, name, decision, revision, instanceId) {
const { database, resolved } = this.prepareWrite(target);
return runOpenClawAgentWriteTransaction((transactionDatabase) => {
if (!hasSession(transactionDatabase, resolved.sessionKey)) throw new BoardValidationError("not_found", `board session not found: ${resolved.sessionKey}`);
const previous = readStoredBoard(transactionDatabase, resolved.sessionKey);
const next = createBoardGrantSnapshot(previous.snapshot, name, decision, revision, instanceId);
upsertTabs(transactionDatabase, previous, next);
const widget = next.widgets.find((candidate) => candidate.name === name);
if (!widget.contentOwner) throw new BoardValidationError("invalid_operation", `board widget ${name} content ownership is unavailable`);
const row = previous.widgetRows.find((candidate) => candidate.name === name);
const manifest = parseManifest(row.manifest);
const declared = manifest.declared;
const db = getNodeSqliteKysely(transactionDatabase.db);
executeSqliteQuerySync(transactionDatabase.db, db.updateTable("board_widgets").set({
grant_state: decision,
granted_sha: decision === "granted" ? row.sha256 : null,
manifest: serializeManifest({
contentOwner: widget.contentOwner,
...widget.registeredContentKind ? { registeredContentKind: widget.registeredContentKind } : {}
}, declared, decision, manifest.mcpAppInteractive !== void 0 && manifest.mcpAppInstanceId ? {
kind: "mcp-app",
interactive: manifest.mcpAppInteractive,
instanceId: manifest.mcpAppInstanceId
} : manifest.registeredInstanceId ? {
kind: "registered",
instanceId: manifest.registeredInstanceId
} : void 0, {
presentation: manifest.presentation,
heightMode: manifest.heightMode
}, manifest.nameIdentity),
updated_at: Date.now()
}).where("session_key", "=", resolved.sessionKey).where("name", "=", name));
return cloneBoardSnapshot(next);
}, {
agentId: resolved.agentId,
path: database.path,
env: this.options.env
}, { operationLabel: "board.grant-widget" });
}
readWidgetRow(target, name) {
const resolved = this.resolve(target);
const result = withOpenClawAgentDatabaseReadOnly((database) => {
if (!hasSession(database, resolved.sessionKey) || !boardTablesPresent(database)) return;
const db = getNodeSqliteKysely(database.db);
return executeSqliteQuerySync(database.db, db.selectFrom("board_widgets").select([
"content_kind",
"html",
"descriptor_json",
"title",
"revision",
"sha256",
"view_generation",
"grant_state",
"manifest"
]).where("session_key", "=", resolved.sessionKey).where("name", "=", name).limit(1)).rows[0];
}, {
agentId: resolved.agentId,
...resolved.path ? { path: resolved.path } : {},
env: this.options.env
});
return result.found ? result.value : void 0;
}
readWidgetHtml(target, name) {
const row = this.readWidgetRow(target, name);
return row ? rowToHtmlDocument(row) : void 0;
}
readWidgetMcpApp(target, name) {
const row = this.readWidgetRow(target, name);
if (!row || row.content_kind !== "mcp-app" || row.descriptor_json === null) return;
const manifest = parseManifest(row.manifest);
if (manifest.mcpAppInteractive === void 0 || manifest.mcpAppInstanceId === void 0) return;
return {
descriptor: parseDescriptor(row.descriptor_json),
revision: row.revision,
instanceId: manifest.mcpAppInstanceId,
grantState: effectiveGrantState(row.grant_state, manifest),
declaredTools: manifest.declared?.tools ?? [],
interactive: manifest.mcpAppInteractive
};
}
readWidgetRegistered(target, name) {
const row = this.readWidgetRow(target, name);
return row ? rowToRegisteredDocument(row) : void 0;
}
};
//#endregion
export { listBoardSessionKeysReadOnly as n, SqliteBoardStore as t };