fix(html-view): re-check workspace confinement on every served request

The serving route checked the blocklist per request but not
attachmentConfineToWorkspace, while the attachment serve path
(file-routes.ts) re-checks it every time. A capability minted for a
folder outside the workspace before an admin turned confinement on kept
serving that folder for the rest of its TTL.

The capability record now says whether its folder was admitted from
outside the workspace, and the serving route refuses such a record while
confinement is on. The flag is part of the reuse key, so an
out-of-workspace mint never picks up a capability recorded as
in-workspace for the same folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-13 17:09:00 +02:00
1 parent fe50ad0be3
commit 876dbca074
4 files changed
+40 -8

No files matched your search

+20
View File
@@ -153,6 +153,26 @@ describe('html-view routes', () => {
expect((await open(join(outsideDir, 'report.html'))).statusCode).toBe(403);
});
it('stops serving an out-of-workspace page once confinement is switched on', async () => {
const res = await open(join(outsideDir, 'report.html'));
expect(res.statusCode).toBe(200);
const url = JSON.parse(res.body).data.url as string;
expect((await harness.app.inject({ method: 'GET', url })).statusCode).toBe(200);
const inside = await openUrl();
process.env.CODEMAN_ATTACHMENT_CONFINE = '1';
expect((await harness.app.inject({ method: 'GET', url })).statusCode).toBe(404);
// A page minted from inside the workspace is not affected.
expect((await harness.app.inject({ method: 'GET', url: inside })).statusCode).toBe(200);
});
it('never reuses an in-workspace capability for the same folder minted from outside', () => {
const inside = htmlViewCapabilities.mint(outsideDir, undefined, false);
const outside = htmlViewCapabilities.mint(outsideDir, undefined, true);
expect(outside).not.toBe(inside);
expect(htmlViewCapabilities.resolve(outside)?.outsideWorkspace).toBe(true);
});
it('refuses every out-of-workspace page when attachments are confined to the workspace', async () => {
process.env.CODEMAN_ATTACHMENT_CONFINE = '1';
expect((await open(join(outsideDir, 'report.html'))).statusCode).toBe(403);