mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
`filterAuthorizedTools` — the control that strips MCP tools a user is not entitled to before an agent is created, updated, duplicated or reverted — exists NOWHERE in this repository. `filterAuthorizedTools.spec.js` imports it from `./v1`; `./v1` does not export it. Another upstream sync that brought the tests and not the code, same shape as requireJwtAuth. That is harmless today, for one reason nobody had written down: the production config declares no `mcpServers:` at all — verified in the running pod, no such block and no mention of mcp. No servers, no MCP tools to attach, nothing to be unauthorized for. The gap is LATENT. The danger is the next person. They enable MCP, they find a 39-test file named after the control, describing exactly the property they want — "should strip unauthorized MCP tools on create", "should store mcpServerNames only for authorized servers" — and they conclude it is enforced. It is not. Eighteen of those tests die on `filterAuthorizedTools is not a function`; the eleven that DO run prove the handlers keep the unauthorized tools and persist them, mcpServerNames and all. So this asserts the PRECONDITION instead of the control: MCP may be configured only once something enforces authorization on it. Deliberately a guard and not an implementation — porting upstream's filter is real work with semantics to get right, and a wrong filter is worse than a missing one because it looks like a control. Proven by mutation: adding an `mcpServers:` block to chat.yaml turns it red, and it goes green again when removed. It also fails the day the filter appears, which is the signal to delete this file and let the real spec do the work. Co-authored-by: Hanzo Dev <[email protected]>
77 lines
3.5 KiB
JavaScript
77 lines
3.5 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* MCP tool authorization does not exist in this fork. This fails the build if
|
|
* anyone enables MCP without it.
|
|
*
|
|
* `filterAuthorizedTools` — upstream's control that strips MCP tools a user is
|
|
* not entitled to before an agent is created, updated, duplicated or reverted —
|
|
* is imported by `filterAuthorizedTools.spec.js` from `./v1`, and `./v1` does
|
|
* not export it. It exists nowhere in this repository. An upstream sync brought
|
|
* the TESTS and not the CODE, the same half-merge that left `requireJwtAuth`
|
|
* with thirteen tests for a middleware it does not have.
|
|
*
|
|
* Right now that is harmless, and only because of a fact nobody wrote down: the
|
|
* production config declares no `mcpServers:` at all (checked in the running
|
|
* pod — no such block, no mention of mcp). With no servers there are no MCP
|
|
* tools to attach and nothing to be unauthorized for. The gap is LATENT.
|
|
*
|
|
* The danger is the next person. They enable MCP, they see a 39-test file
|
|
* called `filterAuthorizedTools.spec.js` describing exactly this control —
|
|
* "should strip unauthorized MCP tools on create", "should store
|
|
* mcpServerNames only for authorized servers" — and they reasonably conclude it
|
|
* is enforced. It is not. Those tests have never run: eighteen die on
|
|
* `filterAuthorizedTools is not a function`, and the eleven that DO run prove
|
|
* the handlers keep the unauthorized tools and persist them, `mcpServerNames`
|
|
* and all.
|
|
*
|
|
* So this asserts the PRECONDITION rather than the control: MCP may be
|
|
* configured only once something enforces authorization on it. It is
|
|
* deliberately a guard and not an implementation — porting upstream's filter is
|
|
* real work with semantics to get right, and a wrong filter is worse than a
|
|
* missing one because it looks like a control.
|
|
*
|
|
* To satisfy this test, do ONE of:
|
|
* - implement + export `filterAuthorizedTools` from `./v1` (then
|
|
* `filterAuthorizedTools.spec.js` becomes the real guard and this can go), or
|
|
* - leave `mcpServers` out of the config.
|
|
*/
|
|
describe('MCP cannot be enabled without tool authorization', () => {
|
|
/** `./v1` is the agents controller the spec imports the filter from. */
|
|
const filterExists = () => {
|
|
try {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return typeof require('./v1').filterAuthorizedTools === 'function';
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** Every config this repo ships that a deployment might actually load. */
|
|
const configs = ['chat.yaml', 'hanzo-chat.example.yaml']
|
|
.map((name) => path.join(__dirname, '..', '..', '..', '..', name))
|
|
.filter((file) => fs.existsSync(file));
|
|
|
|
const declaresMcpServers = (file) =>
|
|
// A top-level `mcpServers:` key — not a mention in a comment.
|
|
/^mcpServers:/m.test(fs.readFileSync(file, 'utf8'));
|
|
|
|
it('finds at least one config to check — otherwise this guard is vacuous', () => {
|
|
expect(configs.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it.each(configs)('%s does not enable MCP while the filter is absent', (file) => {
|
|
if (filterExists()) {
|
|
return; // the control is here; filterAuthorizedTools.spec.js is the guard now
|
|
}
|
|
expect(declaresMcpServers(file)).toBe(false);
|
|
});
|
|
|
|
it('states the precondition it is protecting, so a failure explains itself', () => {
|
|
// If this ever fails it means the filter appeared, which is good news:
|
|
// delete this file and let filterAuthorizedTools.spec.js do the work.
|
|
expect(filterExists()).toBe(false);
|
|
});
|
|
});
|