mindspace: close authority boundaries

This commit is contained in:
john
2026-07-27 15:34:35 +08:00
parent dfab78c75a
commit e94052ff24
78 changed files with 11962 additions and 2162 deletions
+48 -2
View File
@@ -33,8 +33,50 @@ async function exists(targetPath) {
}
}
async function waitForFile(targetPath, { timeoutMs = 5000 } = {}) {
const deadline = Date.now() + timeoutMs;
let lastError = null;
while (Date.now() <= deadline) {
try {
const stat = await fs.stat(targetPath);
if (stat.isFile()) return;
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
throw new Error(
`runtime file was not copied in time: ${targetPath}${
lastError?.message ? ` (${lastError.message})` : ''
}`,
);
}
async function remove(targetPath) {
await fs.rm(targetPath, { recursive: true, force: true });
try {
await fs.rm(targetPath, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
});
return;
} catch (error) {
if (!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(error?.code)) throw error;
const tombstone = `${targetPath}.delete-${process.pid}-${Date.now()}`;
try {
await fs.rename(targetPath, tombstone);
} catch (renameError) {
if (renameError?.code === 'ENOENT') return;
throw error;
}
await fs.rm(tombstone, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 200,
});
}
}
async function copyDir(source, target) {
@@ -48,6 +90,7 @@ function shouldCopyMemindSource(source) {
const parts = relative.split(path.sep);
const top = parts[0];
const last = parts[parts.length - 1];
if (parts.includes('node_modules')) return false;
if (excludeTopLevel.has(top)) return false;
if (last.endsWith('.pid')) return false;
if (last === '.DS_Store' || last === '.env' || last === '.env.local') return false;
@@ -95,6 +138,7 @@ async function copyServiceSource() {
async function copyMemindSourceSnapshot() {
console.log('==> 拷贝 Memind 受控源码快照');
const targetRoot = path.join(runtimeRoot, 'memind-source');
await remove(targetRoot);
await fs.mkdir(targetRoot, { recursive: true });
const entries = await fs.readdir(root);
for (const entry of entries) {
@@ -165,7 +209,9 @@ async function main() {
await copyMemindSourceSnapshot();
await copyNodeModules();
await writeMetadata();
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-mindspace-prod.sh'), 0o755);
const runnerPath = path.join(runtimeRoot, 'scripts', 'run-mindspace-prod.sh');
await waitForFile(runnerPath);
await fs.chmod(runnerPath, 0o755);
console.log('');
console.log(`MindSpace service runtime 已生成: ${runtimeRoot}`);
}
+315
View File
@@ -0,0 +1,315 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createMindSpaceRemoteServerAdapter } from '../mindspace-remote-server-adapter.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
const runtimeRoot = path.join(root, '.runtime', 'mindspace-service');
const localToken = 'local-split-service-smoke-token';
function loadDotenvFile(targetEnv, filepath) {
if (!fs.existsSync(filepath) || !fs.statSync(filepath).isFile()) return;
const content = fs.readFileSync(filepath, 'utf8');
for (const rawLine of content.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line.startsWith('#') || !line.includes('=')) continue;
const [rawKey, ...rawValueParts] = line.split('=');
const key = rawKey.trim();
if (!key || key in targetEnv) continue;
const rawValue = rawValueParts.join('=').trim();
const unwrapped =
(rawValue.startsWith('"') && rawValue.endsWith('"')) ||
(rawValue.startsWith("'") && rawValue.endsWith("'"))
? rawValue.slice(1, -1)
: rawValue;
targetEnv[key] = unwrapped;
}
}
function runBuild() {
const result = spawnSync(
process.execPath,
['scripts/build-mindspace-service-runtime.mjs', '--skip-node-modules'],
{
cwd: root,
stdio: 'inherit',
env: process.env,
},
);
if (result.status !== 0) {
throw new Error(`MindSpace runtime build failed with status ${result.status}`);
}
}
async function freePort() {
const server = net.createServer();
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const port = server.address().port;
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
return port;
}
async function waitForHealth(endpoint, childLog, timeoutMs = 20_000) {
const deadline = Date.now() + timeoutMs;
let lastError = null;
while (Date.now() < deadline) {
try {
const response = await fetch(`${endpoint}/health`);
if (response.ok) return response.json();
lastError = new Error(`health returned ${response.status}`);
} catch (error) {
lastError = error;
}
if (childLog.exited) break;
await new Promise((resolve) => setTimeout(resolve, 250));
}
const detail = childLog.lines.slice(-30).join('\n');
throw new Error(
`MindSpace split-service smoke could not reach /health: ${lastError?.message ?? 'unknown'}${
detail ? `\n--- service log ---\n${detail}` : ''
}`,
);
}
async function stopChild(child) {
if (!child || child.exitCode != null) return;
const waitForExit = () =>
new Promise((resolve) => child.once('exit', resolve));
child.kill('SIGTERM');
await Promise.race([
waitForExit(),
new Promise((resolve) => setTimeout(resolve, 5_000)),
]);
if (child.exitCode != null) return;
child.kill('SIGKILL');
await Promise.race([
waitForExit(),
new Promise((resolve) => setTimeout(resolve, 2_000)),
]);
}
function pageHtml({ title = 'Split Smoke', cover = 'hero.png' } = {}) {
return [
'<!doctype html><html><head>',
`<title>${title}</title>`,
'<meta name="description" content="Split service smoke page">',
`<meta name="mindspace-cover" content='{"tag":"Smoke","accent":"#3366cc","accent2":"#112233","subtitle":"Split service"${cover ? `,"cover":"${cover}"` : ''}}'>`,
'</head><body><main>',
'Split service smoke page ',
'x'.repeat(600),
'</main></body></html>',
].join('');
}
async function resolveSmokeUserId() {
loadDotenvFile(process.env, path.join(root, '.env'));
loadDotenvFile(process.env, path.join(root, '.env.local'));
const db = await import('../db.mjs');
if (!db.isDatabaseConfigured()) {
throw new Error('split-service smoke requires DATABASE_URL or MYSQL_* configuration');
}
const pool = db.createDbPool();
try {
const supplied = String(
process.env.MINDSPACE_SPLIT_SMOKE_USER_ID ??
process.env.MINDSPACE_LOOPBACK_USER_ID ??
'',
).trim();
if (supplied) return { userId: supplied, pool };
const [rows] = await pool.query(
`SELECT id FROM h5_users WHERE status = 'active' ORDER BY created_at DESC LIMIT 1`,
);
const userId = String(rows?.[0]?.id ?? '').trim();
if (!userId) throw new Error('split-service smoke could not find an active local H5 user');
return { userId, pool };
} catch (error) {
await pool.end().catch(() => {});
throw error;
}
}
async function cleanupSmokeRows(pool, { userId, sessionId }) {
const packageId = `cp_${sessionId}`;
const now = Date.now();
await pool.query(`DELETE FROM h5_conversation_artifacts WHERE package_id = ?`, [packageId]).catch(() => {});
await pool.query(
`DELETE FROM h5_conversation_packages WHERE id = ? OR (user_id = ? AND session_id = ?)`,
[packageId, userId, sessionId],
).catch(() => {});
await pool.query(
`UPDATE h5_assets
SET status = 'deleted', deleted_at = ?, updated_at = ?
WHERE user_id = ? AND source_session_id = ?`,
[now, now, userId, sessionId],
).catch(() => {});
}
async function main() {
const skipBuild = process.argv.includes('--skip-build');
if (!skipBuild) runBuild();
const { userId, pool } = await resolveSmokeUserId();
const sessionId = `split-service-smoke-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
const packageId = `cp_${sessionId}`;
const runtimeDataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-split-service-'));
fs.mkdirSync(path.join(runtimeDataRoot, 'MindSpace', userId), { recursive: true });
const port = Number(process.env.MINDSPACE_SPLIT_SMOKE_PORT ?? '') || await freePort();
const endpoint = `http://127.0.0.1:${port}`;
const childLog = { lines: [], exited: false };
const child = spawn(process.execPath, ['server.mjs'], {
cwd: runtimeRoot,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
MINDSPACE_MEMIND_ROOT: path.join(runtimeRoot, 'memind-source'),
MINDSPACE_SERVICE_H5_ROOT: runtimeDataRoot,
MINDSPACE_STORAGE_ROOT: path.join(runtimeDataRoot, 'data', 'mindspace'),
H5_USERS_ROOT: path.join(runtimeDataRoot, 'users'),
MINDSPACE_REMOTE_AUTH_TOKEN: localToken,
MINDSPACE_SERVICE_PORT: String(port),
MINDSPACE_SERVICE_AGENT_WORKER_ENABLED: 'false',
MINDSPACE_SERVICE_WORKSPACE_MAINTENANCE: 'false',
},
});
const capture = (chunk) => {
for (const line of String(chunk).split(/\r?\n/).filter(Boolean)) {
childLog.lines.push(line);
if (childLog.lines.length > 200) childLog.lines.shift();
}
};
child.stdout.on('data', capture);
child.stderr.on('data', capture);
child.once('exit', () => {
childLog.exited = true;
});
try {
const health = await waitForHealth(endpoint, childLog);
assert.equal(health.ok, true);
assert.equal(health.service, 'mindspace-service');
const contractResponse = await fetch(`${endpoint}/mindspace/v1/contract`);
assert.equal(contractResponse.ok, true);
const contract = await contractResponse.json();
assert.ok(contract.bindings.publicFinishService.includes('prepareWechatHtmlDelivery'));
assert.ok(contract.bindings.publicFinishService.includes('ensureWechatFreshPageThumbnails'));
assert.ok(contract.bindings.workspaceToolService.includes('writeFile'));
const remote = createMindSpaceRemoteServerAdapter({
endpoint,
authToken: localToken,
timeoutMs: 20_000,
});
remote.assertReady();
const workspaceRef = `mindspace://users/${userId}/workspace`;
const html = pageHtml();
const write = await remote.workspaceToolService.writeFile({
workspaceRef,
sessionId,
packageId,
path: 'public/split-service-smoke.html',
content: html,
messageId: 'message-workspace-write',
});
assert.equal(write.relativePath, 'public/split-service-smoke.html');
const read = await remote.workspaceToolService.readFile({
workspaceRef,
sessionId,
packageId,
path: 'public/split-service-smoke.html',
});
assert.match(read.content, /Split service smoke page/);
const delivery = await remote.publicFinishService.prepareWechatHtmlDelivery({
userId,
sessionId,
reply: {
text: '页面完成:https://wrong.example/MindSpace/user/public/split-service-smoke.html',
messages: [
{
id: 'message-wechat-write',
role: 'assistant',
content: [
{
type: 'toolRequest',
toolCall: {
value: {
name: 'write_file',
arguments: {
path: 'public/split-service-smoke.html',
content: html,
},
},
},
},
],
},
],
},
intent: {
agentText: '生成 public/split-service-smoke.html',
},
requestStartedAt: Date.now() - 1_000,
allowRecentArtifacts: false,
});
assert.equal(delivery.confirmedArtifacts.length, 1);
assert.equal(delivery.confirmedArtifacts[0].relativePath, 'public/split-service-smoke.html');
assert.equal(Object.hasOwn(delivery.confirmedArtifacts[0], 'localPath'), false);
const thumbnail = await remote.publicFinishService.ensureWechatFreshPageThumbnails({
userId,
sessionId,
artifacts: delivery.confirmedArtifacts,
images: [
{
jobId: 'split-service-smoke-image',
purpose: 'hero',
mimeType: 'image/png',
htmlSrc: 'hero.png',
},
],
});
assert.equal(thumbnail.ok, true);
assert.deepEqual(thumbnail.matchRelativePaths, ['public/split-service-smoke.html']);
assert.ok(thumbnail.thumbnailRelativePaths.includes('public/split-service-smoke.thumbnail.svg'));
assert.equal(JSON.stringify(thumbnail).includes(runtimeDataRoot), false);
console.log(JSON.stringify({
ok: true,
endpoint,
userId,
sessionId,
checks: [
'health',
'contract',
'workspace-write-read',
'wechat-html-delivery',
'wechat-fresh-thumbnail',
],
}, null, 2));
} finally {
await cleanupSmokeRows(pool, { userId, sessionId }).catch(() => {});
await pool.end().catch(() => {});
await stopChild(child);
fs.rmSync(runtimeDataRoot, { recursive: true, force: true });
}
}
main().catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
@@ -0,0 +1,757 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
);
function read(relativePath) {
const targetPath = path.join(root, relativePath);
if (!fs.existsSync(targetPath)) {
throw new Error(
`Missing MindSpace authority guard input: ${relativePath}`,
);
}
return fs.readFileSync(targetPath, 'utf8');
}
function requireSource(source, pattern, label) {
if (!pattern.test(source)) {
throw new Error(`Missing MindSpace authority guard: ${label}`);
}
}
function forbidSource(source, pattern, label) {
if (pattern.test(source)) {
throw new Error(`MindSpace authority leaked back into Portal: ${label}`);
}
}
const portalSource = read('server.mjs');
const contractSource = read(
'mindspace-server-adapter-contract.mjs',
);
const localRuntimeSource = read(
'mindspace-local-runtime-services.mjs',
);
const publicFinishServiceSource = read(
'mindspace-public-finish-service.mjs',
);
const wechatHtmlDeliverySource = read(
'mindspace-wechat-html-delivery.mjs',
);
const portalSessionRoutesSource = read(
'server/portal-session-routes.mjs',
);
const workspaceDeliveryServiceSource = read(
'mindspace-workspace-publication-delivery-service.mjs',
);
const portalWorkspaceDeliverySource = read(
'server/portal-workspace-publication-delivery.mjs',
);
const portalPublicationRoutesSource = read(
'server/portal-publication-routes.mjs',
);
const portalDomainBootstrapSource = read(
'server/portal-domain-services-bootstrap.mjs',
);
const portalGatewayBootstrapSource = read(
'server/portal-gateway-services-bootstrap.mjs',
);
const portalAgentJobRoutesSource = read(
'server/portal-agent-job-routes.mjs',
);
const portalIntegrationBootstrapSource = read(
'server/portal-integration-services-bootstrap.mjs',
);
const wechatMpSource = read('wechat-mp.mjs');
const workspaceToolServiceSource = read(
'mindspace-workspace-tool-service.mjs',
);
const scopedMcpTokenSource = read(
'mindspace-mcp-scoped-token.mjs',
);
const sandboxMcpSource = read(
'mindspace-sandbox-mcp.mjs',
);
const capabilitiesSource = read(
'capabilities.mjs',
);
const userAuthSource = read('user-auth.mjs');
const mindSpaceRpcServerSource = read(
'mindspace-service/mindspace-rpc-server.mjs',
);
const publicationServiceSource = read(
'mindspace-publications.mjs',
);
const withRuntime =
process.argv.includes('--with-runtime') ||
process.env.MINDSPACE_VERIFY_RUNTIME === '1';
requireSource(
portalSource,
/mindSpaceChatSave\.materializeWorkspaceHtml\(/,
'Portal delegates chat-save HTML materialization',
);
forbidSource(
portalSource,
/\bfsPromises\b/,
'server.mjs must not retain the chat-save filesystem writer',
);
forbidSource(
portalSource,
/\bmaterializePrivateAssetsInWorkspaceHtml\b/,
'server.mjs must not materialize private assets directly',
);
forbidSource(
portalSource,
/\brepairMissingHtmlAssetReferences\b/,
'server.mjs must not repair stored HTML directly',
);
forbidSource(
portalSource,
/\bresolveMindSpaceUserPublishDir\b/,
'server.mjs must not resolve MindSpace workspace directories',
);
forbidSource(
portalSource,
/\blistRecentlyModifiedPublicHtmlRelativePaths\b/,
'server.mjs must not scan workspace HTML directly',
);
requireSource(
portalSource,
/\.listRecentlyModifiedPublicHtml\(\{/,
'Portal delegates recent workspace discovery to MindSpace',
);
const chatSaveRoutesSource = read(
'server/portal-mindspace-chat-save-routes.mjs',
);
const chatShareRoutesSource = read(
'server/portal-mindspace-chat-share-routes.mjs',
);
const portalAssetRoutesSource = read(
'server/portal-mindspace-asset-routes.mjs',
);
forbidSource(
chatSaveRoutesSource,
/\bresolveMindSpaceUserPublishDir\b/,
'chat-save routes must not resolve workspace publish directories',
);
forbidSource(
chatSaveRoutesSource,
/\bensureWorkspaceHtmlThumbnail\b/,
'chat-save routes must not write workspace thumbnails directly',
);
requireSource(
chatSaveRoutesSource,
/getMindSpaceChatSave/,
'chat-save routes delegate preview thumbnails to MindSpace',
);
requireSource(
chatSaveRoutesSource,
/chatSave\.ensurePreviewThumbnail\(/,
'chat-save routes use MindSpace preview thumbnail authority',
);
requireSource(
chatSaveRoutesSource,
/chatSave\.renderPreviewThumbnailSvg\(/,
'chat-save routes render preview thumbnails through MindSpace',
);
requireSource(
chatShareRoutesSource,
/chatSave\.createSharedHtml\(\{/,
'quick share delegates shared HTML creation to MindSpace',
);
forbidSource(
chatShareRoutesSource,
/\bwriteFile\b/,
'chat share routes must not write shared HTML directly',
);
forbidSource(
chatShareRoutesSource,
/\bbuildMindSpacePublicUrlForUser\b/,
'chat share routes must not build public URLs',
);
forbidSource(
chatShareRoutesSource,
/\bresolveMindSpaceRuntimeConfig\b/,
'chat share routes must not resolve storage roots',
);
forbidSource(
chatShareRoutesSource,
/\bresolveMindSpaceUserPublishDir\b/,
'chat share routes must not resolve workspace publish directories',
);
forbidSource(
chatShareRoutesSource,
/\binlinePrivateAssetsInHtml\b/,
'chat share routes must not localize private assets directly',
);
requireSource(
contractSource,
/chatSaveService:\s*Object\.freeze\(\[/,
'local and remote adapters expose chatSaveService',
);
requireSource(
contractSource,
/'materializeWorkspaceHtml'/,
'chat save contract includes workspace HTML authority',
);
requireSource(
contractSource,
/'ensurePreviewThumbnail'/,
'chat save contract includes preview thumbnail authority',
);
requireSource(
contractSource,
/'renderPreviewThumbnailSvg'/,
'chat save contract includes preview thumbnail rendering',
);
requireSource(
contractSource,
/'createSharedHtml'/,
'chat save contract includes shared HTML authority',
);
requireSource(
contractSource,
/'readWorkspaceHtml'/,
'chat save contract includes workspace HTML read authority',
);
requireSource(
localRuntimeSource,
/createMindSpaceChatSaveService\(\{/,
'MindSpace local runtime owns chatSaveService',
);
requireSource(
portalAssetRoutesSource,
/assets\.readAssetContent\(/,
'Portal asset download consumes logical MindSpace content',
);
requireSource(
portalAssetRoutesSource,
/\.readPublicAssetContent\(/,
'Portal public asset download consumes logical MindSpace content',
);
forbidSource(
portalAssetRoutesSource,
/(?:node:fs|sendFile\(|\breadFile\()/,
'Portal asset routes must not read physical asset paths',
);
forbidSource(
contractSource,
/'read(?:Public)?Asset'/,
'remote asset contract must not expose physical readAsset paths',
);
requireSource(
contractSource,
/'readAssetContent'/,
'remote asset contract exposes logical asset content',
);
requireSource(
portalSource,
/registerPublishedLongImageArtifact\(\{/,
'Portal delegates published long image registration',
);
forbidSource(
portalSource,
/mindSpaceConversationPackageRegistry\.(?:putObjectForSession|recordArtifact|writeManifestForSession)\(/,
'server.mjs must not mutate conversation packages directly',
);
forbidSource(
portalSessionRoutesSource,
/\bmaterializePublicHtmlWritesFromSessionEvent\b/,
'Portal session routes must not materialize public HTML directly',
);
forbidSource(
portalSessionRoutesSource,
/\bsyncPublicHtmlAfterFinish\b/,
'Portal session routes must not perform final public HTML sync directly',
);
forbidSource(
portalSessionRoutesSource,
/\bPUBLISH_ROOT_DIR\b/,
'Portal session routes must not build public page URLs',
);
forbidSource(
portalSessionRoutesSource,
/(?:resolveMindSpaceUserPublishDir|resolveMindSpaceRuntimeConfig|\bpublishDir\b|\bstorageRoot\b)/,
'Portal session routes must not resolve MindSpace storage paths',
);
requireSource(
portalSessionRoutesSource,
/publicFinish\.materializeSessionEvent\(\{/,
'streamed public HTML writes delegate to MindSpace',
);
requireSource(
portalSessionRoutesSource,
/publicFinish\.syncAfterFinish\(\{/,
'Finish public HTML sync delegates to MindSpace',
);
requireSource(
portalSessionRoutesSource,
/evaluation:\s*[\s\S]{0,80}syncResult[\s\S]{0,80}deliveryEvaluation/,
'Portal consumes MindSpace HTML delivery evaluation',
);
requireSource(
portalSessionRoutesSource,
/publicFinish\.preparePageDataAfterFinish\(\{/,
'Portal requests Page Data preparation from MindSpace after page sync',
);
requireSource(
chatSaveRoutesSource,
/conversationArtifacts\.registerChatDocxArtifact\(\{/,
'chat DOCX artifact registration delegates to MindSpace',
);
forbidSource(
chatSaveRoutesSource,
/getConversationPackageRegistry/,
'chat save routes must not access the package registry directly',
);
requireSource(
contractSource,
/conversationArtifactService:\s*Object\.freeze\(\[/,
'local and remote adapters expose conversationArtifactService',
);
requireSource(
contractSource,
/'registerPublicHtmlArtifacts'/,
'conversation artifact contract includes public HTML registration',
);
forbidSource(
contractSource,
/conversationPackageRegistry:\s*Object\.freeze\(\[[\s\S]{0,240}'(?:ensurePackage|putObjectForSession|recordArtifact|writeManifestForSession)'/,
'remote package registry contract must stay read-only',
);
requireSource(
localRuntimeSource,
/createMindSpaceConversationArtifactService\(\{/,
'MindSpace local runtime owns conversationArtifactService',
);
requireSource(
contractSource,
/publicFinishService:\s*Object\.freeze\(\[/,
'local and remote adapters expose publicFinishService',
);
requireSource(
contractSource,
/'preparePageDataAfterFinish'/,
'public Finish contract includes Page Data preparation',
);
requireSource(
contractSource,
/'prepareWechatPageDataDelivery'/,
'public Finish contract includes WeChat Page Data delivery preparation',
);
requireSource(
contractSource,
/'prepareWechatHtmlDelivery'/,
'public Finish contract includes WeChat HTML delivery preparation',
);
requireSource(
contractSource,
/'ensureWechatFreshPageThumbnails'/,
'public Finish contract includes WeChat thumbnail authority',
);
requireSource(
localRuntimeSource,
/createMindSpacePublicFinishService\(\{/,
'MindSpace local runtime owns publicFinishService',
);
requireSource(
publicFinishServiceSource,
/conversationArtifactService[\s\S]{0,160}\.registerPublicHtmlArtifacts\(\{/,
'MindSpace Finish service owns public HTML artifact registration',
);
requireSource(
publicFinishServiceSource,
/evaluateH5HtmlFinishGuardFn\(\{/,
'MindSpace Finish service owns HTML delivery evaluation',
);
requireSource(
publicFinishServiceSource,
/preparePageDataAfterFinishFn\(\{/,
'MindSpace Finish service owns Page Data delivery preparation',
);
requireSource(
publicFinishServiceSource,
/resolvePageDataCollectOutcomeAsyncFn\(\{/,
'MindSpace Finish service owns WeChat Page Data delivery evaluation',
);
requireSource(
publicFinishServiceSource,
/ensurePageDataDeliveryReadyFn\(\{/,
'MindSpace Finish service owns WeChat Page Data backing delivery checks',
);
forbidSource(
portalIntegrationBootstrapSource,
/resolveMindSpaceRuntimeConfig|\bstorageRoot\b|\bauthPool\b/,
'Portal integration bootstrap must not configure WeChat with MindSpace storage paths',
);
requireSource(
portalIntegrationBootstrapSource,
/pageDataFinishGuard:[\s\S]{0,180}mindSpacePublicFinish/,
'Portal gives WeChat the logical MindSpace Finish authority',
);
requireSource(
portalIntegrationBootstrapSource,
/htmlDeliveryAuthority:[\s\S]{0,260}mindSpacePublicFinish/,
'Portal gives WeChat the logical MindSpace HTML delivery authority',
);
forbidSource(
wechatMpSource,
/pageDataFinishGuard\?\.(?:pool|h5Root|storageRoot)/,
'WeChat delivery must not receive MindSpace storage internals',
);
requireSource(
wechatMpSource,
/prepareWechatPageDataDelivery/,
'WeChat delegates Page Data delivery preparation to MindSpace',
);
requireSource(
wechatMpSource,
/prepareWechatHtmlDelivery/,
'WeChat delegates HTML delivery preparation to MindSpace',
);
requireSource(
wechatMpSource,
/ensureWechatFreshPageThumbnails/,
'WeChat delegates fresh thumbnail delivery to MindSpace',
);
forbidSource(
wechatMpSource,
/materializeMissingPublicHtmlWrites|ensureWorkspaceHtmlThumbnail|collectRecentPublishedHtmlArtifacts|resolveLinkExistsForWorkingDir/,
'WeChat must not perform physical HTML delivery work',
);
requireSource(
publicFinishServiceSource,
/prepareWechatHtmlDeliveryAtWorkspaceFn\(\{/,
'MindSpace Finish service owns WeChat HTML backing-file preparation',
);
requireSource(
publicFinishServiceSource,
/ensureWechatFreshPageThumbnailsAtWorkspaceFn/,
'MindSpace Finish service owns WeChat fresh thumbnail work',
);
requireSource(
wechatHtmlDeliverySource,
/materializeMissingPublicHtmlWrites\(\{/,
'MindSpace WeChat delivery materializes HTML inside the service boundary',
);
requireSource(
wechatHtmlDeliverySource,
/ensureWorkspaceHtmlThumbnail\(/,
'MindSpace WeChat delivery renders thumbnails inside the service boundary',
);
requireSource(
contractSource,
/workspacePublicationDeliveryService:\s*[\s\S]{0,80}Object\.freeze\(\[/,
'local and remote adapters expose workspacePublicationDeliveryService',
);
requireSource(
contractSource,
/'resolveWorkspaceRequest'/,
'workspace delivery contract includes logical request resolution',
);
requireSource(
contractSource,
/'listRecentlyModifiedPublicHtml'/,
'workspace delivery contract includes recent HTML discovery',
);
requireSource(
localRuntimeSource,
/createMindSpaceWorkspacePublicationDeliveryService\(\{/,
'MindSpace local runtime owns workspace publication delivery',
);
requireSource(
contractSource,
/workspacePageDeliveryService:\s*[\s\S]{0,80}Object\.freeze\(\['syncAndDeliver'\]\)/,
'local and remote adapters expose workspace page delivery',
);
requireSource(
localRuntimeSource,
/createWorkspacePageDeliverService\(\{/,
'MindSpace local runtime owns workspace page sync and delivery',
);
requireSource(
portalDomainBootstrapSource,
/\.workspacePageDeliveryService/,
'Portal domain bootstrap consumes the MindSpace workspace page delivery binding',
);
forbidSource(
portalDomainBootstrapSource,
/(?:createWorkspacePageDeliverService|resolveMindSpaceRuntimeConfig|ensurePageDataHtmlPagesBound)/,
'Portal domain bootstrap must not construct storage-aware workspace delivery',
);
requireSource(
portalGatewayBootstrapSource,
/service\.validateRunDeliverables\(/,
'Portal run validation delegates to MindSpace',
);
forbidSource(
portalGatewayBootstrapSource,
/(?:resolveMindSpaceUserPublishDir|evaluatePageDataHtmlContent|readPageAccessPolicy|scanWorkspaceFilesForProhibitedBrowserStorage)/,
'Portal gateway must not read workspace files for delivery validation',
);
forbidSource(
portalGatewayBootstrapSource,
/(?:mindSpaceAssets\.readAsset\(|readAssetFileFn)/,
'Portal gateway must not read physical MindSpace asset paths',
);
forbidSource(
portalAgentJobRoutesSource,
/(?:node:fs|\breadFile\(|asset\.path)/,
'Portal agent job routes must not read physical asset paths',
);
requireSource(
portalAgentJobRoutesSource,
/asset\.bodyBase64/,
'Portal agent job delivery consumes MindSpace binary content',
);
requireSource(
workspaceDeliveryServiceSource,
/resolvePublicRequestFn\(\{/,
'MindSpace workspace delivery resolves public backing files',
);
requireSource(
workspaceDeliveryServiceSource,
/getDeliveryContractFn\(\{/,
'MindSpace workspace delivery validates delivery readiness',
);
requireSource(
workspaceDeliveryServiceSource,
/materializePrivateAssetsFn\(\{/,
'MindSpace workspace delivery owns private asset materialization',
);
requireSource(
workspaceDeliveryServiceSource,
/async validateRunDeliverables\(\{/,
'MindSpace workspace delivery owns run deliverable validation',
);
requireSource(
portalWorkspaceDeliverySource,
/\.resolveWorkspaceRequest\(\{/,
'Portal workspace route delegates logical delivery to MindSpace',
);
requireSource(
portalWorkspaceDeliverySource,
/\.renderLongImage\(\{/,
'Portal delegates workspace long-image rendering to MindSpace',
);
forbidSource(
portalWorkspaceDeliverySource,
/(?:node:fs|node:path|resolveMindSpaceUserPublishDir|resolveMindSpaceRuntimeConfig|resolveMindSpacePublicRequest|readFileSync|sendFile\()/,
'Portal workspace delivery must not resolve or read physical backing files',
);
forbidSource(
portalWorkspaceDeliverySource,
/materializePrivateAssetsInWorkspaceHtml/,
'Portal workspace delivery must not materialize private assets',
);
requireSource(
portalPublicationRoutesSource,
/\.readOwnerPublicAsset\(\{/,
'owner public asset route delegates to MindSpace',
);
forbidSource(
portalPublicationRoutesSource,
/(?:node:fs|node:path|resolveMindSpaceUserPublishDir|sendFile\()/,
'Portal publication routes must not resolve or serve physical files',
);
forbidSource(
portalPublicationRoutesSource,
/\bbuildMindSpacePublicRoutePath\b/,
'Portal publication routes must not construct workspace canonical URLs',
);
requireSource(
portalPublicationRoutesSource,
/result\.workspacePublicUrl/,
'Portal publication redirects use the MindSpace canonical URL',
);
requireSource(
publicationServiceSource,
/workspacePublicUrl:\s*[\s\S]{0,80}resolveWorkspaceMindSpacePublicUrl\(\{/,
'MindSpace publication resolve result owns the workspace canonical URL',
);
requireSource(
contractSource,
/workspaceToolService:\s*Object\.freeze\(\[/,
'local and remote adapters expose logical workspace tools',
);
requireSource(
contractSource,
/workspaceToolService:[\s\S]{0,240}'generateLongImage'[\s\S]{0,240}'writeBinaryFile'/,
'workspace tool contract includes long-image generation and binary writes',
);
requireSource(
contractSource,
/conversationArtifactService:[\s\S]{0,240}'registerWorkspaceFileArtifact'/,
'conversation artifact contract includes generic workspace write registration',
);
requireSource(
localRuntimeSource,
/createMindSpaceWorkspaceToolService\(\{/,
'MindSpace local runtime owns logical workspace tools',
);
requireSource(
workspaceToolServiceSource,
/parseMindSpaceWorkspaceRef\(workspaceRef\)/,
'workspace tools resolve a logical workspaceRef',
);
requireSource(
workspaceToolServiceSource,
/syncWorkspaceAssets\(/,
'workspace writes synchronize logical assets',
);
requireSource(
workspaceToolServiceSource,
/\.registerWorkspaceFileArtifact\(\{/,
'non-public workspace writes register package artifacts directly',
);
requireSource(
workspaceToolServiceSource,
/\.registerPublicHtmlArtifacts\(\{/,
'public HTML workspace writes register package artifacts immediately',
);
requireSource(
workspaceToolServiceSource,
/async writeBinaryFile\([\s\S]{0,900}registerWrite\(\{/,
'binary workspace writes persist and register through MindSpace',
);
requireSource(
workspaceToolServiceSource,
/async generateLongImage\([\s\S]{0,1800}renderLongImageFn\(\{[\s\S]{0,900}registerWrite\(\{/,
'long-image generation and artifact registration run inside MindSpace',
);
forbidSource(
workspaceToolServiceSource,
/\babsolutePath\s*:/,
'logical workspace tool results must not expose absolute paths',
);
requireSource(
scopedMcpTokenSource,
/sessionId:[\s\S]{0,180}packageId:[\s\S]{0,180}workspaceRef:[\s\S]{0,180}tools:/,
'MCP token binds session, package, workspace, and tool allowlist',
);
requireSource(
mindSpaceRpcServerSource,
/verifyMindSpaceMcpScopedToken\(\{/,
'MindSpace MCP RPC verifies scoped tokens',
);
requireSource(
mindSpaceRpcServerSource,
/workspaceToolService[\s\S]{0,240}claims\.workspaceRef/,
'MindSpace MCP RPC injects token workspace claims into tool calls',
);
requireSource(
mindSpaceRpcServerSource,
/generate_long_image:\s*'generateLongImage'[\s\S]{0,160}write_binary_file:\s*'writeBinaryFile'/,
'MindSpace MCP RPC exposes scoped long-image and binary operations',
);
requireSource(
sandboxMcpSource,
/callLogicalWorkspaceTool\(/,
'sandbox MCP delegates logical workspace operations over RPC',
);
requireSource(
sandboxMcpSource,
/MINDSPACE_MCP_SCOPED_TOKEN/,
'sandbox MCP accepts only a scoped service token',
);
requireSource(
sandboxMcpSource,
/case 'generate_docx':[\s\S]{0,1200}callLogicalWorkspaceTool\([\s\S]{0,120}'write_binary_file'/,
'scoped DOCX generation uploads through MindSpace instead of the workspace path',
);
requireSource(
sandboxMcpSource,
/LOGICAL_WORKSPACE_TOOLS[\s\S]{0,240}'generate_long_image'/,
'scoped long-image generation delegates to MindSpace',
);
requireSource(
capabilitiesSource,
/mintMindSpaceMcpScopedToken\(\{/,
'Portal policy assembly mints a scoped MCP token',
);
requireSource(
capabilitiesSource,
/mcpTools\.includes\('generate_docx'\)[\s\S]{0,100}'write_binary_file'/,
'DOCX capability grants only the internal scoped binary write operation',
);
forbidSource(
capabilitiesSource,
/envs\.MINDSPACE_MCP_TOKEN_SECRET/,
'the MCP signing secret must never enter the Goose extension environment',
);
requireSource(
userAuthSource,
/workspaceRef:[\s\S]{0,180}sessionId:[\s\S]{0,180}packageId:/,
'agent session policy supplies logical workspace/session/package scope',
);
requireSource(
userAuthSource,
/mcpBaseUrl:[\s\S]{0,160}mcpTokenSecret:/,
'agent session policy supplies the MCP endpoint and server-side signing secret',
);
if (withRuntime) {
const portalRuntimeSource = read('.runtime/portal/server.mjs');
const mindSpaceRuntimeContract = read(
'.runtime/mindspace-service/memind-source/mindspace-server-adapter-contract.mjs',
);
const mindSpaceRuntimeService = read(
'.runtime/mindspace-service/memind-source/mindspace-chat-save-service.mjs',
);
const mindSpaceRuntimeArtifactService = read(
'.runtime/mindspace-service/memind-source/mindspace-conversation-package-artifact-service.mjs',
);
const mindSpaceRuntimePublicFinishService = read(
'.runtime/mindspace-service/memind-source/mindspace-public-finish-service.mjs',
);
const mindSpaceRuntimeWorkspaceDeliveryService = read(
'.runtime/mindspace-service/memind-source/mindspace-workspace-publication-delivery-service.mjs',
);
requireSource(
portalRuntimeSource,
/materializeWorkspaceHtml/,
'Portal runtime delegates chat-save materialization',
);
requireSource(
mindSpaceRuntimeContract,
/chatSaveService/,
'MindSpace runtime exposes the chat-save RPC contract',
);
requireSource(
mindSpaceRuntimeService,
/writeFileFn\(absolutePath,\s*materializedHtml,\s*'utf8'\)/,
'MindSpace runtime owns the chat-save workspace write',
);
requireSource(
mindSpaceRuntimeService,
/async createSharedHtml\(\{/,
'MindSpace runtime owns shared HTML creation',
);
requireSource(
mindSpaceRuntimeArtifactService,
/registerPublicHtmlArtifacts/,
'MindSpace runtime owns conversation artifact registration',
);
requireSource(
mindSpaceRuntimePublicFinishService,
/syncAfterFinish/,
'MindSpace runtime owns public Finish synchronization',
);
requireSource(
mindSpaceRuntimeWorkspaceDeliveryService,
/resolveWorkspaceRequest/,
'MindSpace runtime owns workspace publication delivery',
);
}
console.log(
withRuntime
? 'mindspace read/write authority boundary ok (with runtimes)'
: 'mindspace read/write authority boundary ok',
);
@@ -19,11 +19,32 @@ run('MindSpace publish + chat finish unit tests', [
'--test',
'mindspace-public-finish-sync.test.mjs',
'mindspace-h5-html-finish-guard.test.mjs',
'mindspace-chat-save-service.test.mjs',
'mindspace-conversation-package-artifact-service.test.mjs',
'mindspace-workspace-publication-delivery-service.test.mjs',
'mindspace-workspace-tool-service.test.mjs',
'mindspace-mcp-scoped-token.test.mjs',
'mindspace-sandbox-mcp.test.mjs',
'capabilities.test.mjs',
'server/portal-workspace-publication-delivery.test.mjs',
'server/portal-publication-routes.test.mjs',
'server/portal-mindspace-asset-routes.test.mjs',
'server/portal-agent-job-routes.test.mjs',
'server/portal-gateway-services-bootstrap.test.mjs',
'conversation-display.test.mjs',
'chat-finish-sync.test.mjs',
]);
run('chat finish sync source guards', ['scripts/verify-chat-finish-sync.mjs']);
run(
withRuntime
? 'MindSpace authority source and runtime guards'
: 'MindSpace authority source guards',
[
'scripts/verify-mindspace-authority-boundary.mjs',
...(withRuntime ? ['--with-runtime'] : []),
],
);
if (withRuntime) {
run('public finish sync runtime guard', ['scripts/verify-public-finish-sync-runtime.mjs']);
+13 -10
View File
@@ -62,19 +62,22 @@ if (!fs.existsSync(runtimeServer)) {
}
const runtimeSource = fs.readFileSync(runtimeServer, 'utf8');
const requiredRuntimeSnippets = [
'normalizedName === "write"',
'normalizedName.endsWith("__write")',
'normalizedName === "edit_file"',
'applyPublicHtmlEdit',
'recentCount = 80',
'if (extractPublicHtmlWriteArtifacts([message], { publishDir }).length > 0) {',
const requiredRuntimePatterns = [
[/normalizedName === "write"/, 'plain write tool request detection'],
[/normalizedName\.endsWith\("__write"\)/, 'namespaced write tool request detection'],
[/normalizedName === "edit_file"/, 'edit_file tool request detection'],
[/applyPublicHtmlEdit/, 'public HTML edit materialization'],
[/recentCount = 80/, 'wide recent message scan window'],
[
/if \(extractPublicHtmlWriteArtifacts\(\[message\], \{ publishDir(?:: \w+)? \}\)\.length > 0\) \{/,
'textless session-event HTML write prefilter',
],
];
for (const snippet of requiredRuntimeSnippets) {
for (const [pattern, label] of requiredRuntimePatterns) {
assert.ok(
runtimeSource.includes(snippet),
`runtime server bundle is missing public finish sync guard: ${snippet}`,
pattern.test(runtimeSource),
`runtime server bundle is missing public finish sync guard: ${label}`,
);
}