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
+191 -3
View File
@@ -3,6 +3,16 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MCP_TOOL_METHODS = Object.freeze({
create_dir: 'createDirectory',
edit_file: 'editFile',
generate_long_image: 'generateLongImage',
list_dir: 'listDirectory',
publish_page: 'publishPage',
read_file: 'readFile',
write_binary_file: 'writeBinaryFile',
write_file: 'writeFile',
});
async function loadContract(env = process.env) {
const memindRoot = path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source');
@@ -11,19 +21,48 @@ async function loadContract(env = process.env) {
);
}
async function loadMcpScopedTokenVerifier(env = process.env) {
const memindRoot = path.resolve(__dirname, env.MINDSPACE_MEMIND_ROOT ?? '../memind-source');
return import(
pathToFileURL(path.join(memindRoot, 'mindspace-mcp-scoped-token.mjs')).href
);
}
function parseBearerToken(headers) {
const auth = String(headers.authorization ?? '');
if (!auth.startsWith('Bearer ')) return '';
return auth.slice('Bearer '.length).trim();
}
async function readJsonBody(req) {
async function readJsonBody(
req,
{ maxBytes = 32 * 1024 * 1024 } = {},
) {
const chunks = [];
let receivedBytes = 0;
for await (const chunk of req) {
chunks.push(chunk);
const buffer = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk);
receivedBytes += buffer.length;
if (receivedBytes > maxBytes) {
throw Object.assign(
new Error('MindSpace RPC request body is too large'),
{ code: 'request_body_too_large' },
);
}
chunks.push(buffer);
}
const text = Buffer.concat(chunks).toString('utf8').trim();
return text ? JSON.parse(text) : {};
if (!text) return {};
try {
return JSON.parse(text);
} catch {
throw Object.assign(
new Error('MindSpace RPC request body must be valid JSON'),
{ code: 'invalid_json_body' },
);
}
}
function reviveRpcValue(value) {
@@ -67,17 +106,39 @@ function resolveRpcErrorStatus(error) {
case 'publication_owner_not_found':
case 'page_not_found':
case 'category_not_found':
case 'static_page_not_found':
return 404;
case 'publication_login_required':
return 401;
case 'publication_password_required':
case 'mcp_tool_forbidden':
case 'mcp_scope_mismatch':
return 403;
case 'invalid_mcp_token':
case 'expired_mcp_token':
return 401;
case 'invalid_input':
case 'invalid_page_input':
case 'invalid_page_path':
case 'empty_page_content':
case 'invalid_publish_input':
case 'invalid_state_transition':
case 'slug_conflict':
case 'security_ack_required':
case 'invalid_workspace_tool_input':
case 'invalid_workspace_ref':
case 'invalid_workspace_path':
case 'invalid_workspace_package_scope':
case 'invalid_workspace_user_scope':
case 'invalid_workspace_page_path':
case 'invalid_workspace_binary':
case 'invalid_json_body':
return 400;
case 'workspace_entry_not_found':
return 404;
case 'workspace_file_too_large':
case 'request_body_too_large':
return 413;
default:
return 500;
}
@@ -99,6 +160,35 @@ export async function createMindSpaceRpcRequestHandler({
.trim()
.replace(/\/+$/, '') || '/mindspace/v1/adapter';
const authToken = String(env.MINDSPACE_REMOTE_AUTH_TOKEN ?? '').trim();
const mcpOperationBasePath = String(
serviceMeta.mcpOperationBasePath ??
env.MINDSPACE_MCP_OPERATION_BASE_PATH ??
'/mindspace/v1/mcp',
)
.trim()
.replace(/\/+$/, '') ||
'/mindspace/v1/mcp';
const mcpTokenSecret = String(
env.MINDSPACE_MCP_TOKEN_SECRET ?? '',
).trim();
if (
mcpTokenSecret &&
mcpTokenSecret.length < 16
) {
throw new Error(
'MINDSPACE_MCP_TOKEN_SECRET must contain at least 16 characters',
);
}
const mcpTokenVerifier = mcpTokenSecret
? await loadMcpScopedTokenVerifier(env)
: null;
const mcpMaxBodyBytes = Math.max(
1024,
Number(
env.MINDSPACE_MCP_MAX_BODY_BYTES ??
12 * 1024 * 1024,
) || 12 * 1024 * 1024,
);
return async function handleMindSpaceRpc(req, res) {
try {
@@ -110,6 +200,9 @@ export async function createMindSpaceRpcRequestHandler({
adapterKind: adapter.kind,
implementationStatus: adapter.implementationStatus,
operationBasePath,
mcpOperationBasePath,
mcpScopedToolsEnabled:
Boolean(mcpTokenSecret),
backgroundJobsManagedLocally: adapter.kind === 'local',
});
}
@@ -119,6 +212,101 @@ export async function createMindSpaceRpcRequestHandler({
bindingKeys: MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
});
}
if (
req.method === 'POST' &&
url.pathname.startsWith(
`${mcpOperationBasePath}/`,
)
) {
if (!mcpTokenSecret) {
return json(res, 503, {
message:
'MindSpace MCP scoped tokens are not configured',
});
}
const segments = url.pathname
.slice(
mcpOperationBasePath.length + 1,
)
.split('/')
.filter(Boolean);
if (segments.length !== 1) {
return json(res, 400, {
message:
'Expected /mindspace/v1/mcp/:tool',
});
}
const tool = decodeURIComponent(
segments[0],
);
const method = MCP_TOOL_METHODS[tool];
if (
!method ||
typeof adapter?.workspaceToolService?.[
method
] !== 'function'
) {
return json(res, 404, {
message: `Unknown MCP tool: ${tool}`,
});
}
const claims =
mcpTokenVerifier.verifyMindSpaceMcpScopedToken({
token: parseBearerToken(
req.headers,
),
secret: mcpTokenSecret,
tool,
});
const body = await readJsonBody(req, {
maxBytes: mcpMaxBodyBytes,
});
const suppliedInput =
body?.arguments &&
typeof body.arguments === 'object'
? reviveRpcValue(body.arguments)
: {};
for (const [
field,
expected,
] of Object.entries({
userId: claims.userId,
sessionId: claims.sessionId,
packageId: claims.packageId,
workspaceRef:
claims.workspaceRef,
})) {
const supplied = String(
suppliedInput?.[field] ?? '',
).trim();
if (
supplied &&
supplied !== expected
) {
throw Object.assign(
new Error(
`${field} does not match the scoped MCP token`,
),
{ code: 'mcp_scope_mismatch' },
);
}
}
const result =
await adapter.workspaceToolService[
method
]({
...suppliedInput,
userId: claims.userId,
sessionId: claims.sessionId,
packageId: claims.packageId,
workspaceRef:
claims.workspaceRef,
});
return json(res, 200, {
ok: true,
result,
});
}
if (req.method !== 'POST' || !url.pathname.startsWith(`${operationBasePath}/`)) {
return json(res, 404, { message: 'Not found' });
}
+629 -2
View File
@@ -1,10 +1,18 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
mintMindSpaceMcpScopedToken,
} from '../mindspace-mcp-scoped-token.mjs';
import { createMindSpaceRpcRequestHandler } from './mindspace-rpc-server.mjs';
function createStubAdapter() {
const calls = {
writeUploadContent: [],
registerChatDocxArtifact: [],
workspaceTools: [],
};
return {
calls,
@@ -15,6 +23,175 @@ function createStubAdapter() {
return [];
},
},
chatSaveService: {
async createSharedHtml() {
return {
publicUrl: 'https://example.com/shared.html',
filename: 'shared.html',
};
},
async materializeWorkspaceHtml() {
return { changed: false };
},
async readWorkspaceHtml() {
return {
html: '<html></html>',
relativePath: 'public/page.html',
};
},
async ensurePreviewThumbnail() {
return { ready: true };
},
async renderPreviewThumbnailSvg() {
return { svg: '<svg></svg>', contentType: 'image/svg+xml; charset=utf-8' };
},
},
conversationArtifactService: {
async registerChatDocxArtifact(...args) {
calls.registerChatDocxArtifact.push(args);
return { id: 'docx-1' };
},
async registerPublicHtmlArtifacts() {
return [{ artifactId: 'html-1' }];
},
async registerPublishedLongImageArtifact() {
return { id: 'image-1' };
},
async registerWorkspaceFileArtifact() {
return { id: 'file-1' };
},
},
publicFinishService: {
async materializeSessionEvent() {
return { materialized: [], skipped: [] };
},
async syncAfterFinish() {
return {
materialized: [],
skipped: [],
publicHtmlRelativePaths: [],
};
},
async preparePageDataAfterFinish() {
return {
autoBind: {
bound: [],
skipped: [],
errors: [],
},
evaluation: {
structuralPageData: false,
relevantFiles: [],
},
};
},
async prepareWechatPageDataDelivery() {
return {
outcome: { action: 'skip' },
deliveryArtifacts: [],
};
},
async prepareWechatHtmlDelivery() {
return {
confirmedArtifacts: [],
};
},
async ensureWechatFreshPageThumbnails() {
return { ok: true };
},
},
workspacePublicationDeliveryService: {
async listRecentlyModifiedPublicHtml() {
return { relativePaths: [] };
},
async readOwnerPublicAsset() {
return { action: 'not_found' };
},
async renderLongImage() {
return {
bodyBase64: '',
servedName: 'page.long.png',
};
},
async resolveWorkspaceRequest() {
return { action: 'not_found' };
},
async validateRunDeliverables() {
return { errors: [] };
},
},
workspacePageDeliveryService: {
async syncAndDeliver() {
return { sync: {} };
},
},
workspaceToolService: {
async createDirectory() {
calls.workspaceTools.push([
'createDirectory',
...arguments,
]);
return { relativePath: 'public/assets' };
},
async editFile() {
calls.workspaceTools.push([
'editFile',
...arguments,
]);
return { relativePath: 'public/page.html' };
},
async generateLongImage() {
calls.workspaceTools.push([
'generateLongImage',
...arguments,
]);
return {
relativePath:
'public/page.long.png',
};
},
async listDirectory() {
calls.workspaceTools.push([
'listDirectory',
...arguments,
]);
return { entries: [] };
},
async publishPage() {
calls.workspaceTools.push([
'publishPage',
...arguments,
]);
return {
canonicalUrl:
'https://example.com/MindSpace/user-1/public/page.html',
};
},
async readFile() {
calls.workspaceTools.push([
'readFile',
...arguments,
]);
return { content: 'hello' };
},
async writeFile() {
calls.workspaceTools.push([
'writeFile',
...arguments,
]);
return { relativePath: 'public/page.html' };
},
async writeBinaryFile() {
calls.workspaceTools.push([
'writeBinaryFile',
...arguments,
]);
return {
relativePath:
'public/report.docx',
};
},
},
conversationPackageRegistry: {
async ensurePackage() { return { ok: true }; },
async putObjectForSession() { return { ok: true }; },
@@ -31,8 +208,12 @@ function createStubAdapter() {
async createUpload() { return { ok: true }; },
async deleteAsset() { return { ok: true }; },
async listAssets() { return []; },
async readAsset() { return { ok: true }; },
async readPublicAsset() { return { ok: true }; },
async readAssetContent() {
return { ok: true, bodyBase64: '' };
},
async readPublicAssetContent() {
return { ok: true, bodyBase64: '' };
},
async renderAssetPreview() { return { preview: true }; },
async renderAssetThumbnail() { return { thumbnail: true }; },
async syncWorkspaceAssets() { return { synced: true }; },
@@ -148,6 +329,107 @@ test('health and contract endpoints are exposed without auth', async () => {
assert.equal(health.body.ok, true);
assert.equal(contract.statusCode, 200);
assert.ok(contract.body.bindings.assetService.includes('renderAssetPreview'));
assert.ok(
contract.body.bindings.chatSaveService.includes(
'materializeWorkspaceHtml',
),
);
assert.ok(
contract.body.bindings.conversationArtifactService
.includes('registerPublicHtmlArtifacts'),
);
assert.ok(
contract.body.bindings.publicFinishService.includes(
'syncAfterFinish',
),
);
assert.ok(
contract.body.bindings.publicFinishService.includes(
'preparePageDataAfterFinish',
),
);
assert.ok(
contract.body.bindings.publicFinishService.includes(
'prepareWechatPageDataDelivery',
),
);
assert.ok(
contract.body.bindings.publicFinishService.includes(
'prepareWechatHtmlDelivery',
),
);
assert.ok(
contract.body.bindings.publicFinishService.includes(
'ensureWechatFreshPageThumbnails',
),
);
assert.ok(
contract.body.bindings
.workspacePublicationDeliveryService
.includes('resolveWorkspaceRequest'),
);
assert.ok(
contract.body.bindings
.workspaceToolService
.includes('writeBinaryFile'),
);
assert.ok(
contract.body.bindings
.workspaceToolService
.includes('generateLongImage'),
);
});
test('RPC startup rejects an undersized MCP signing secret', async () => {
await assert.rejects(
createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_MCP_TOKEN_SECRET:
'too-short',
},
}),
/at least 16 characters/,
);
});
test('RPC startup does not require MCP token verifier when scoped MCP is disabled', async (t) => {
const memindRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'mindspace-rpc-no-mcp-'),
);
t.after(() => {
fs.rmSync(memindRoot, {
recursive: true,
force: true,
});
});
fs.copyFileSync(
path.resolve(
'mindspace-server-adapter-contract.mjs',
),
path.join(
memindRoot,
'mindspace-server-adapter-contract.mjs',
),
);
const handler =
await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: memindRoot,
},
});
const health = await runRequest(handler, {
method: 'GET',
path: '/health',
});
assert.equal(health.statusCode, 200);
assert.equal(
health.body.mcpScopedToolsEnabled,
false,
);
});
test('rpc invocation enforces auth token and dispatches args to adapter method', async () => {
@@ -199,6 +481,39 @@ test('rpc invocation revives JSON-serialized buffers before dispatch', async ()
assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true);
});
test('rpc revives generated artifact bodies before dispatching to MindSpace', async () => {
const adapter = createStubAdapter();
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const payload = {
userId: 'user-1',
sessionId: 'session-1',
messageId: 'message-1',
filename: 'chat.docx',
body: Buffer.from('docx payload'),
};
const response = await runRequest(handler, {
method: 'POST',
path:
'/mindspace/v1/adapter/conversationArtifactService/registerChatDocxArtifact',
body: JSON.stringify({ args: [payload] }),
});
assert.equal(response.statusCode, 200);
assert.deepEqual(response.body, { id: 'docx-1' });
assert.equal(
Buffer.isBuffer(
adapter.calls.registerChatDocxArtifact[0][0].body,
),
true,
);
});
test('rpc maps publication_not_found to 404 with error code', async () => {
const adapter = createStubAdapter();
adapter.publicationService.resolvePublic = async () => {
@@ -247,3 +562,315 @@ test('rpc maps publication_login_required to 401 with error code', async () => {
assert.equal(response.statusCode, 401);
assert.equal(response.body.code, 'publication_login_required');
});
test('rpc maps chat save validation errors to 400 with error code', async () => {
const adapter = createStubAdapter();
adapter.chatSaveService.materializeWorkspaceHtml =
async () => {
const error = new Error('无效的页面路径');
error.code = 'invalid_page_path';
throw error;
};
const handler = await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
},
});
const response = await runRequest(handler, {
method: 'POST',
path:
'/mindspace/v1/adapter/chatSaveService/materializeWorkspaceHtml',
body: JSON.stringify({
args: [{
userId: 'user-1',
html: '<html>unsafe</html>',
relativePath: '../unsafe.html',
}],
}),
});
assert.equal(response.statusCode, 400);
assert.equal(response.body.code, 'invalid_page_path');
});
test('scoped MCP RPC injects token claims and dispatches only allowed workspace tools', async () => {
const adapter = createStubAdapter();
const secret =
'mindspace-mcp-test-secret-1234';
const token = mintMindSpaceMcpScopedToken({
secret,
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
tools: ['write_file'],
});
const handler =
await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_MCP_TOKEN_SECRET:
secret,
},
});
const response = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/mcp/write_file',
headers: {
authorization: `Bearer ${token}`,
},
body: JSON.stringify({
arguments: {
path: 'public/page.html',
content: '<html></html>',
},
}),
});
assert.equal(response.statusCode, 200);
assert.equal(response.body.ok, true);
assert.deepEqual(
adapter.calls.workspaceTools[0],
[
'writeFile',
{
path: 'public/page.html',
content: '<html></html>',
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
},
],
);
});
test('scoped MCP RPC dispatches binary writes and long-image generation to MindSpace', async () => {
const adapter = createStubAdapter();
const secret =
'mindspace-mcp-test-secret-1234';
const token = mintMindSpaceMcpScopedToken({
secret,
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
tools: [
'generate_long_image',
'write_binary_file',
],
});
const handler =
await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_MCP_TOKEN_SECRET:
secret,
},
});
const headers = {
authorization: `Bearer ${token}`,
};
const binary = await runRequest(handler, {
method: 'POST',
path:
'/mindspace/v1/mcp/write_binary_file',
headers,
body: JSON.stringify({
arguments: {
path: 'public/report.docx',
bodyBase64:
Buffer.from('PK').toString(
'base64',
),
},
}),
});
const longImage = await runRequest(
handler,
{
method: 'POST',
path:
'/mindspace/v1/mcp/generate_long_image',
headers,
body: JSON.stringify({
arguments: {
html_path:
'public/page.html',
output_path:
'public/page.long.png',
},
}),
},
);
assert.equal(binary.statusCode, 200);
assert.equal(longImage.statusCode, 200);
assert.deepEqual(
adapter.calls.workspaceTools.map(
([method]) => method,
),
[
'writeBinaryFile',
'generateLongImage',
],
);
assert.equal(
adapter.calls.workspaceTools[0][1]
.packageId,
'cp_session-1',
);
});
test('scoped MCP RPC rejects tools and scope overrides outside the token', async () => {
const adapter = createStubAdapter();
const secret =
'mindspace-mcp-test-secret-1234';
const readToken =
mintMindSpaceMcpScopedToken({
secret,
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
tools: ['read_file'],
});
const writeToken =
mintMindSpaceMcpScopedToken({
secret,
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
tools: ['write_file'],
});
const handler =
await createMindSpaceRpcRequestHandler({
adapter,
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_MCP_TOKEN_SECRET:
secret,
},
});
const forbidden = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/mcp/write_file',
headers: {
authorization: `Bearer ${readToken}`,
},
body: JSON.stringify({
arguments: {
path: 'public/page.html',
content: '<html></html>',
},
}),
});
const scopeMismatch = await runRequest(
handler,
{
method: 'POST',
path:
'/mindspace/v1/mcp/write_file',
headers: {
authorization: `Bearer ${writeToken}`,
},
body: JSON.stringify({
arguments: {
workspaceRef:
'mindspace://users/user-2/workspace',
path: 'public/page.html',
content: '<html></html>',
},
}),
},
);
assert.equal(forbidden.statusCode, 403);
assert.equal(
forbidden.body.code,
'mcp_tool_forbidden',
);
assert.equal(scopeMismatch.statusCode, 403);
assert.equal(
scopeMismatch.body.code,
'mcp_scope_mismatch',
);
assert.equal(
adapter.calls.workspaceTools.length,
0,
);
});
test('scoped MCP RPC bounds request bodies and reports invalid JSON as client errors', async () => {
const secret =
'mindspace-mcp-test-secret-1234';
const token = mintMindSpaceMcpScopedToken({
secret,
userId: 'user-1',
sessionId: 'session-1',
packageId: 'cp_session-1',
workspaceRef:
'mindspace://users/user-1/workspace',
tools: ['write_file'],
});
const handler =
await createMindSpaceRpcRequestHandler({
adapter: createStubAdapter(),
env: {
MINDSPACE_MEMIND_ROOT: '..',
MINDSPACE_MCP_TOKEN_SECRET:
secret,
MINDSPACE_MCP_MAX_BODY_BYTES:
'1024',
},
});
const headers = {
authorization: `Bearer ${token}`,
};
const tooLarge = await runRequest(handler, {
method: 'POST',
path: '/mindspace/v1/mcp/write_file',
headers,
body: JSON.stringify({
arguments: {
path: 'oa/large.txt',
content: 'x'.repeat(1200),
},
}),
});
const invalidJson = await runRequest(
handler,
{
method: 'POST',
path:
'/mindspace/v1/mcp/write_file',
headers,
body: '{invalid',
},
);
assert.equal(tooLarge.statusCode, 413);
assert.equal(
tooLarge.body.code,
'request_body_too_large',
);
assert.equal(invalidJson.statusCode, 400);
assert.equal(
invalidJson.body.code,
'invalid_json_body',
);
});
@@ -54,7 +54,6 @@ async function loadMemindModules(memindRoot) {
config,
conversationMemory,
localServerAdapter,
publicHtmlHelpers,
runtimeConfig,
sessionSnapshot,
userAuth,
@@ -67,7 +66,6 @@ async function loadMemindModules(memindRoot) {
importFromMemindRoot(memindRoot, 'mindspace-config.mjs'),
importFromMemindRoot(memindRoot, 'conversation-memory.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-local-server-adapter.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-conversation-package-public-html.mjs'),
importFromMemindRoot(memindRoot, 'mindspace-runtime-config.mjs'),
importFromMemindRoot(memindRoot, 'session-snapshot.mjs'),
importFromMemindRoot(memindRoot, 'user-auth.mjs'),
@@ -81,7 +79,6 @@ async function loadMemindModules(memindRoot) {
...config,
...conversationMemory,
...localServerAdapter,
...publicHtmlHelpers,
...runtimeConfig,
...sessionSnapshot,
...userAuth,
@@ -137,7 +134,6 @@ export async function bootstrapMindSpaceService({
createSessionSnapshotService,
createUserAuth,
createExperienceService,
registerPublicHtmlArtifactsForConversationPackage,
resolveMindSpaceServerRuntimeOptions,
startWorkspaceAssetSyncWatcher,
startWorkspaceThumbnailWatcher,
@@ -172,13 +168,6 @@ export async function bootstrapMindSpaceService({
resolveUserIdForAgentSession: (sessionId) => resolveUserIdForAgentSession(pool, sessionId),
resolveWorkspaceRoot: (userId) => userAuth.resolveWorkingDir(userId),
resolveSessionSnapshot: (sessionId) => sessionSnapshotService.get(sessionId),
registerPublicHtmlArtifactsForConversation: (payload) =>
registerPublicHtmlArtifactsForConversationPackage({
conversationPackageRegistry: adapter.conversationPackageRegistry,
h5Root: standaloneDataRoot,
env,
...payload,
}),
logger,
});