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
+58 -103
View File
@@ -19,6 +19,13 @@ function createSetup(overrides = {}) {
const tkmindProxy = { id: 'proxy' };
const toolGateway = { id: 'tool-gateway' };
const agentRunGateway = { id: 'agent-run-gateway' };
const workspacePublicationDelivery = {
async validateRunDeliverables() {
return { errors: [] };
},
};
const getWorkspacePublicationDelivery =
() => workspacePublicationDelivery;
let proxyOptions;
let toolOptions;
let agentOptions;
@@ -48,14 +55,21 @@ function createSetup(overrides = {}) {
id: 'system-disclosure-policy',
},
mindSpaceAssets: {
async readAsset(userId, assetId) {
async readAssetContent(
userId,
assetId,
) {
calls.push(['read-asset', userId, assetId]);
return {
asset: { mimeType: 'image/png' },
path: '/assets/image.png',
bodyBase64:
Buffer.from('image').toString(
'base64',
),
};
},
},
getWorkspacePublicationDelivery,
directChatService: { id: 'direct-chat' },
chatIntentRouter: { id: 'intent-router' },
async syncUserGeneratedPages(userId, options) {
@@ -86,10 +100,6 @@ function createSetup(overrides = {}) {
validatorOptions = receivedOptions;
return async () => ({ errors: [] });
},
async readAssetFileFn(assetPath) {
calls.push(['read-asset-file', assetPath]);
return Buffer.from('image');
},
...overrides,
};
return {
@@ -103,6 +113,8 @@ function createSetup(overrides = {}) {
tkmindProxy,
toolGateway,
agentRunGateway,
workspacePublicationDelivery,
getWorkspacePublicationDelivery,
getCaptured() {
return {
proxyOptions,
@@ -155,7 +167,8 @@ test('preserves Proxy, Tool, and Agent gateway wiring', () => {
llmProviderService: setup.llmProviderService,
});
assert.deepEqual(captured.validatorOptions, {
h5Root: '/app',
getWorkspacePublicationDelivery:
setup.getWorkspacePublicationDelivery,
});
assert.equal(captured.agentOptions.tkmindProxy, setup.tkmindProxy);
assert.equal(captured.agentOptions.toolGateway, setup.toolGateway);
@@ -235,11 +248,8 @@ test('preserves local asset reads and optional asset absence', async () => {
},
);
assert.deepEqual(
setup.calls.slice(-2),
[
['read-asset', 'user-1', 'asset-1'],
['read-asset-file', '/assets/image.png'],
],
setup.calls.slice(-1),
[['read-asset', 'user-1', 'asset-1']],
);
const withoutAssets = createSetup({
@@ -327,48 +337,28 @@ test('keeps memory observation optional and parses disabled dispatch', async ()
assert.equal(agentOptions.runTimeoutMs, 15 * 60 * 1000);
});
test('validates Page Data issues, policy grants, and browser storage', async () => {
const policyChecks = [];
test('delegates run deliverable validation to MindSpace', async () => {
const calls = [];
const validator = createPortalRunDeliverablesValidator({
h5Root: '/app',
resolveMindSpaceUserPublishDirFn: () =>
'/publish/user-1',
normalizeWorkspaceRelativePathFn: (value) => value,
resolvePathFn: (...parts) => parts.join('/').replaceAll('//', '/'),
pathSeparator: '/',
existsSyncFn: () => true,
readFileSyncFn: () => '<html>page data</html>',
evaluatePageDataHtmlContentFn: () => ({
usesPageDataApi: true,
issues: ['dataset_binding_missing'],
}),
readPageAccessPolicyFn: () => ({ id: 'policy' }),
detectPageDataDatasetUsageFromHtmlFn: () => [
[
'orders',
{ read: true, insert: true },
],
],
policyAllowsActionFn(policy, dataset, action) {
policyChecks.push([policy, dataset, action]);
return action === 'read';
},
scanWorkspaceFilesForProhibitedBrowserStorageFn:
(options) => {
assert.deepEqual(options, {
publishDir: '/publish/user-1',
relativePaths: ['public/orders.html'],
});
return [
{
relativePath: 'public/orders.html',
apis: ['localStorage', 'indexedDB'],
},
];
},
getWorkspacePublicationDelivery:
() => ({
async validateRunDeliverables(
input,
) {
calls.push(input);
return {
errors: [
{
code:
'browser_storage_forbidden',
},
],
};
},
}),
});
const result = await validator({
const input = {
userId: 'user-1',
deliverables: {
pages: [
@@ -378,63 +368,28 @@ test('validates Page Data issues, policy grants, and browser storage', async ()
},
],
},
});
};
const result = await validator(input);
assert.deepEqual(policyChecks, [
[{ id: 'policy' }, 'orders', 'read'],
[{ id: 'policy' }, 'orders', 'insert'],
]);
assert.deepEqual(
result.errors.map(({ code }) => code),
[
'dataset_binding_missing',
'page_data_policy_action_missing',
'browser_storage_forbidden',
],
assert.deepEqual(calls, [input]);
assert.equal(
result.errors[0].code,
'browser_storage_forbidden',
);
});
test('skips non-public, missing, and path-escaping Page Data files', async () => {
const readPaths = [];
test('fails closed when MindSpace validation is unavailable', async () => {
const validator = createPortalRunDeliverablesValidator({
h5Root: '/app',
resolveMindSpaceUserPublishDirFn: () =>
'/publish/user-1',
normalizeWorkspaceRelativePathFn: (value) => value,
resolvePathFn: (...parts) => {
const joined = parts.join('/');
if (joined.includes('escape')) return '/outside/file.html';
return joined.replaceAll('//', '/');
},
pathSeparator: '/',
existsSyncFn: (filePath) =>
!filePath.includes('missing'),
readFileSyncFn(filePath) {
readPaths.push(filePath);
return '<html></html>';
},
evaluatePageDataHtmlContentFn: () => ({
usesPageDataApi: false,
issues: [],
}),
scanWorkspaceFilesForProhibitedBrowserStorageFn:
() => [],
getWorkspacePublicationDelivery:
() => null,
});
const result = await validator({
userId: 'user-1',
deliverables: {
pages: [
{ workspaceRelativePath: 'private/a.html' },
{ workspaceRelativePath: 'public/missing.html' },
{ workspaceRelativePath: 'public/escape.html' },
{ workspaceRelativePath: 'public/valid.html' },
],
},
});
assert.deepEqual(readPaths, [
'/publish/user-1/public/valid.html',
]);
assert.deepEqual(result, { errors: [] });
await assert.rejects(
() =>
validator({
userId: 'user-1',
deliverables: { pages: [] },
}),
/交付验证服务未启用/,
);
});