fix(mindspace): fail closed on invalid page data delivery
This commit is contained in:
+38
-4
@@ -138,8 +138,11 @@ import {
|
||||
} from './mindspace-public-finish-sync.mjs';
|
||||
import { getPageDeliveryContract, markPageDeliveryContractReady, preparePageDeliveryContract } from './mindspace-delivery-contract.mjs';
|
||||
import { maybeRepairH5HtmlAfterFinish } from './mindspace-h5-html-finish-guard.mjs';
|
||||
import { maybeRepairPageDataAfterFinish } from './mindspace-page-data-finish-guard.mjs';
|
||||
import { evaluatePageDataHtmlContent, maybeRepairPageDataAfterFinish } from './mindspace-page-data-finish-guard.mjs';
|
||||
import { ensurePageDataHtmlPagesBound } from './page-data-workspace-ensure.mjs';
|
||||
import { detectPageDataDatasetUsageFromHtml } from './page-data-html-detect.mjs';
|
||||
import { readPageAccessPolicy } from './page-data-policy-store.mjs';
|
||||
import { policyAllowsAction } from './page-access-policy.mjs';
|
||||
import { quickPlazaFromChat, quickPlazaFromPublicHtml, getQuickPlazaFromPublicHtmlStatus } from './mindspace-chat-plaza.mjs';
|
||||
import { injectPublicFileShareButton } from './mindspace-public-share-widget.mjs';
|
||||
import { resolvePlazaPostPath, resolvePlazaPublicBase } from './src/utils/public-site-bases.mjs';
|
||||
@@ -736,6 +739,30 @@ async function bootstrapUserAuth() {
|
||||
isSessionExternallyBusy: ({ sessionId }) => Number(sessionPageDeliveryLocks.get(sessionId) ?? 0) > 0,
|
||||
validateRunDeliverables: async ({ userId, deliverables }) => {
|
||||
const publishDir = resolveMindSpaceUserPublishDir(__dirname, { id: userId });
|
||||
const pageDataErrors = [];
|
||||
for (const page of deliverables?.pages ?? []) {
|
||||
const relativePath = normalizeWorkspaceRelativePath(page.workspaceRelativePath);
|
||||
if (!relativePath?.startsWith('public/')) continue;
|
||||
const filePath = path.resolve(publishDir, relativePath);
|
||||
if (!filePath.startsWith(`${path.resolve(publishDir)}${path.sep}`) || !fs.existsSync(filePath)) continue;
|
||||
const html = fs.readFileSync(filePath, 'utf8');
|
||||
const evaluation = evaluatePageDataHtmlContent(html, { relativePath });
|
||||
if (!evaluation.usesPageDataApi) continue;
|
||||
for (const issue of evaluation.issues) {
|
||||
pageDataErrors.push({ code: issue, message: `${relativePath} Page Data HTML 不可交付:${issue}` });
|
||||
}
|
||||
const policy = page.pageId ? readPageAccessPolicy(publishDir, page.pageId) : null;
|
||||
for (const [dataset, actions] of detectPageDataDatasetUsageFromHtml(html)) {
|
||||
for (const action of ['read', 'insert']) {
|
||||
if (actions?.[action] && !policyAllowsAction(policy, dataset, action)) {
|
||||
pageDataErrors.push({
|
||||
code: 'page_data_policy_action_missing',
|
||||
message: `${relativePath} 的 ${dataset}.${action} 未获最终 policy 授权或 dataset 已关闭`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const violations = scanWorkspaceFilesForProhibitedBrowserStorage({
|
||||
publishDir,
|
||||
relativePaths: (deliverables?.pages ?? [])
|
||||
@@ -743,10 +770,10 @@ async function bootstrapUserAuth() {
|
||||
.filter(Boolean),
|
||||
});
|
||||
return {
|
||||
errors: violations.map((violation) => ({
|
||||
errors: [...pageDataErrors, ...violations.map((violation) => ({
|
||||
code: 'browser_storage_forbidden',
|
||||
message: `${violation.relativePath} 使用 ${violation.apis.join(', ')}`,
|
||||
})),
|
||||
}))],
|
||||
};
|
||||
},
|
||||
autoDispatch: ['1', 'true', 'yes', 'on'].includes(
|
||||
@@ -3286,9 +3313,16 @@ async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null
|
||||
// Agent-run/Finish delivery must stay scoped to the current conversation.
|
||||
// A stale Page Data page elsewhere in the user's workspace must not turn a
|
||||
// successfully completed current task into a failed run.
|
||||
const pageDataRelativePaths = sessionId
|
||||
const discoveredRelativePaths = sessionId
|
||||
? await listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs })
|
||||
: null;
|
||||
// An empty artifact package does not prove that this run wrote no page: a
|
||||
// normal static-page-publish flow may only leave a public HTML workspace
|
||||
// file. `null` means "discover current workspace output"; `[]` would tell
|
||||
// the sync service to inspect nothing and make a completed page run fail.
|
||||
const pageDataRelativePaths = discoveredRelativePaths?.length
|
||||
? discoveredRelativePaths
|
||||
: null;
|
||||
if (workspacePageDeliver?.syncAndDeliver) {
|
||||
return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user