mindspace: close authority boundaries
This commit is contained in:
@@ -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: [] },
|
||||
}),
|
||||
/交付验证服务未启用/,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user