mindspace: close authority boundaries
This commit is contained in:
@@ -176,6 +176,422 @@ test('sandbox MCP rejects browser storage in generated page code', async (t) =>
|
||||
assert.doesNotMatch(fs.readFileSync(path.join(root, 'public', 'ledger.html'), 'utf8'), /indexedDB/);
|
||||
});
|
||||
|
||||
test('sandbox MCP delegates scoped workspace tools to MindSpace without local file writes', async (t) => {
|
||||
const requests = [];
|
||||
const api = http.createServer((req, res) => {
|
||||
let body = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
const parsedBody = JSON.parse(body);
|
||||
requests.push({
|
||||
url: req.url,
|
||||
authorization:
|
||||
req.headers.authorization,
|
||||
body: parsedBody,
|
||||
});
|
||||
const tool = req.url.split('/').at(-1);
|
||||
if (
|
||||
tool === 'generate_long_image' &&
|
||||
parsedBody.arguments
|
||||
?.output_path ===
|
||||
'public/fail.long.png'
|
||||
) {
|
||||
res.statusCode = 503;
|
||||
res.setHeader(
|
||||
'content-type',
|
||||
'application/json',
|
||||
);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
code:
|
||||
'long_image_unavailable',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const results = {
|
||||
write_file: {
|
||||
relativePath: 'public/page.html',
|
||||
packageId: 'cp_session-1',
|
||||
sizeBytes: 20,
|
||||
},
|
||||
read_file: {
|
||||
relativePath: 'public/page.html',
|
||||
packageId: 'cp_session-1',
|
||||
content: '<html>remote</html>',
|
||||
},
|
||||
publish_page: {
|
||||
relativePath: 'public/page.html',
|
||||
packageId: 'cp_session-1',
|
||||
canonicalUrl:
|
||||
'https://example.com/MindSpace/user-1/public/page.html',
|
||||
},
|
||||
generate_long_image: {
|
||||
relativePath:
|
||||
'public/page.long.png',
|
||||
packageId: 'cp_session-1',
|
||||
sizeBytes: 4096,
|
||||
canonicalUrl:
|
||||
'https://example.com/MindSpace/user-1/public/page.long.png',
|
||||
},
|
||||
};
|
||||
res.setHeader(
|
||||
'content-type',
|
||||
'application/json',
|
||||
);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
result: results[tool] ?? {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
api.listen(0, '127.0.0.1');
|
||||
await once(api, 'listening');
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
'mindspace-sandbox-rpc-',
|
||||
),
|
||||
);
|
||||
const sandbox = startSandbox(root, {
|
||||
ALLOWED_TOOLS:
|
||||
'read_file,write_file,publish_page,generate_long_image',
|
||||
MINDSPACE_MCP_BASE_URL:
|
||||
`http://127.0.0.1:${api.address().port}`,
|
||||
MINDSPACE_MCP_SCOPED_TOKEN:
|
||||
'scoped-token',
|
||||
MINDSPACE_WORKSPACE_REF:
|
||||
'mindspace://users/user-1/workspace',
|
||||
MINDSPACE_SESSION_ID: 'session-1',
|
||||
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
||||
});
|
||||
t.after(() => sandbox.child.kill());
|
||||
t.after(() => api.close());
|
||||
|
||||
await sandbox.request('initialize');
|
||||
const written = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'write_file',
|
||||
arguments: {
|
||||
path: 'public/page.html',
|
||||
content: '<html>remote</html>',
|
||||
},
|
||||
},
|
||||
);
|
||||
const read = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'read_file',
|
||||
arguments: {
|
||||
path: 'public/page.html',
|
||||
},
|
||||
},
|
||||
);
|
||||
const published = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'publish_page',
|
||||
arguments: {
|
||||
path: 'public/page.html',
|
||||
},
|
||||
},
|
||||
);
|
||||
const longImage = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'generate_long_image',
|
||||
arguments: {
|
||||
html_path: 'public/page.html',
|
||||
output_path:
|
||||
'public/page.long.png',
|
||||
},
|
||||
},
|
||||
);
|
||||
const failedLongImage =
|
||||
await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'generate_long_image',
|
||||
arguments: {
|
||||
html_path:
|
||||
'public/page.html',
|
||||
output_path:
|
||||
'public/fail.long.png',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(written.result.isError, false);
|
||||
assert.match(
|
||||
written.result.content[0].text,
|
||||
/package cp_session-1/,
|
||||
);
|
||||
assert.equal(
|
||||
read.result.content[0].text,
|
||||
'<html>remote</html>',
|
||||
);
|
||||
assert.match(
|
||||
published.result.content[0].text,
|
||||
/canonicalUrl/,
|
||||
);
|
||||
assert.match(
|
||||
longImage.result.content[0].text,
|
||||
/page\.long\.png/,
|
||||
);
|
||||
assert.equal(
|
||||
failedLongImage.result.isError,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(root, 'public', 'page.html'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'public',
|
||||
'page.long.png',
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'public',
|
||||
'fail.long.png',
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.deepEqual(
|
||||
requests.map((request) => request.url),
|
||||
[
|
||||
'/mindspace/v1/mcp/write_file',
|
||||
'/mindspace/v1/mcp/read_file',
|
||||
'/mindspace/v1/mcp/publish_page',
|
||||
'/mindspace/v1/mcp/generate_long_image',
|
||||
'/mindspace/v1/mcp/generate_long_image',
|
||||
],
|
||||
);
|
||||
assert.ok(
|
||||
requests.every(
|
||||
(request) =>
|
||||
request.authorization ===
|
||||
'Bearer scoped-token',
|
||||
),
|
||||
);
|
||||
assert.deepEqual(requests[0].body, {
|
||||
arguments: {
|
||||
path: 'public/page.html',
|
||||
content: '<html>remote</html>',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('generate_docx uploads its binary through scoped MindSpace RPC without writing the target workspace path', async (t) => {
|
||||
const requests = [];
|
||||
const api = http.createServer((req, res) => {
|
||||
let body = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
requests.push({
|
||||
url: req.url,
|
||||
authorization:
|
||||
req.headers.authorization,
|
||||
body: JSON.parse(body),
|
||||
});
|
||||
res.setHeader(
|
||||
'content-type',
|
||||
'application/json',
|
||||
);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
result: {
|
||||
relativePath:
|
||||
'public/report.docx',
|
||||
packageId:
|
||||
'cp_session-1',
|
||||
sizeBytes: 1024,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
api.listen(0, '127.0.0.1');
|
||||
await once(api, 'listening');
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
'mindspace-sandbox-docx-rpc-',
|
||||
),
|
||||
);
|
||||
copyDocxGenerateSkill(root);
|
||||
const sandbox = startSandbox(root, {
|
||||
ALLOWED_TOOLS: 'generate_docx',
|
||||
MINDSPACE_MCP_BASE_URL:
|
||||
`http://127.0.0.1:${api.address().port}`,
|
||||
MINDSPACE_MCP_SCOPED_TOKEN:
|
||||
'scoped-token',
|
||||
MINDSPACE_WORKSPACE_REF:
|
||||
'mindspace://users/user-1/workspace',
|
||||
MINDSPACE_SESSION_ID: 'session-1',
|
||||
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
||||
});
|
||||
t.after(() => sandbox.child.kill());
|
||||
t.after(() => api.close());
|
||||
|
||||
await sandbox.request('initialize');
|
||||
const generated = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'generate_docx',
|
||||
arguments: {
|
||||
output_path:
|
||||
'public/report.docx',
|
||||
title: 'Scoped report',
|
||||
sections:
|
||||
summarySections('Scoped report'),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
generated.result.isError,
|
||||
false,
|
||||
generated.result.content?.[0]?.text,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'public',
|
||||
'report.docx',
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
requests[0].url,
|
||||
'/mindspace/v1/mcp/write_binary_file',
|
||||
);
|
||||
assert.equal(
|
||||
requests[0].authorization,
|
||||
'Bearer scoped-token',
|
||||
);
|
||||
assert.equal(
|
||||
requests[0].body.arguments.path,
|
||||
'public/report.docx',
|
||||
);
|
||||
const uploaded = Buffer.from(
|
||||
requests[0].body.arguments
|
||||
.bodyBase64,
|
||||
'base64',
|
||||
);
|
||||
assert.equal(
|
||||
uploaded.subarray(0, 2).toString(
|
||||
'utf8',
|
||||
),
|
||||
'PK',
|
||||
);
|
||||
assert.ok(uploaded.length > 500);
|
||||
});
|
||||
|
||||
test('scoped DOCX generation fails closed when MindSpace rejects the binary write', async (t) => {
|
||||
const api = http.createServer(
|
||||
(req, res) => {
|
||||
req.resume();
|
||||
req.on('end', () => {
|
||||
res.statusCode = 503;
|
||||
res.setHeader(
|
||||
'content-type',
|
||||
'application/json',
|
||||
);
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
ok: false,
|
||||
code:
|
||||
'mindspace_unavailable',
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
api.listen(0, '127.0.0.1');
|
||||
await once(api, 'listening');
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(
|
||||
os.tmpdir(),
|
||||
'mindspace-sandbox-docx-fail-',
|
||||
),
|
||||
);
|
||||
copyDocxGenerateSkill(root);
|
||||
const sandbox = startSandbox(root, {
|
||||
ALLOWED_TOOLS: 'generate_docx',
|
||||
MINDSPACE_MCP_BASE_URL:
|
||||
`http://127.0.0.1:${api.address().port}`,
|
||||
MINDSPACE_MCP_SCOPED_TOKEN:
|
||||
'scoped-token',
|
||||
MINDSPACE_WORKSPACE_REF:
|
||||
'mindspace://users/user-1/workspace',
|
||||
MINDSPACE_SESSION_ID: 'session-1',
|
||||
MINDSPACE_PACKAGE_ID: 'cp_session-1',
|
||||
});
|
||||
t.after(() => sandbox.child.kill());
|
||||
t.after(() => api.close());
|
||||
|
||||
await sandbox.request('initialize');
|
||||
const generated = await sandbox.request(
|
||||
'tools/call',
|
||||
{
|
||||
name: 'generate_docx',
|
||||
arguments: {
|
||||
output_path:
|
||||
'public/report.docx',
|
||||
title: 'Rejected report',
|
||||
sections:
|
||||
summarySections(
|
||||
'Rejected report',
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
generated.result.isError,
|
||||
true,
|
||||
);
|
||||
assert.match(
|
||||
generated.result.content[0].text,
|
||||
/mindspace_unavailable/,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'public',
|
||||
'report.docx',
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('sandbox MCP exposes and protects the user private data space', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mindspace-sandbox-'));
|
||||
const server = startSandbox(root);
|
||||
|
||||
Reference in New Issue
Block a user