Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "context-room",
"version": "0.6.14",
"version": "0.6.15",
"description": "Local-first documentation control room for AI-assisted projects.",
"type": "module",
"homepage": "https://www.npmjs.com/package/context-room",
Expand Down
14 changes: 9 additions & 5 deletions src/context_room.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22425,7 +22425,8 @@ async function routeRequest(req, res, root, globalPreferencesPath = null, {
return;
}
if (req.method === "GET" && url.pathname === "/api/startup-context") {
sendJson(res, 200, { files: listStartupContextFiles(root).map(publicStartupContextFile), root });
const settings = readResolvedContextRoomSettings(root, { expectedRootIdentity });
sendJson(res, 200, { files: listStartupContextFiles(root, settings).map(publicStartupContextFile), root });
return;
}
if (req.method === "GET" && url.pathname === "/api/startup-skills") {
Expand Down Expand Up @@ -22465,14 +22466,16 @@ async function routeRequest(req, res, root, globalPreferencesPath = null, {
}
if (req.method === "GET" && url.pathname === "/api/startup-context/file") {
const order = url.searchParams.get("order") || "";
sendJson(res, 200, readStartupContextFile(root, order));
const settings = readResolvedContextRoomSettings(root, { expectedRootIdentity });
sendJson(res, 200, readStartupContextFile(root, order, settings));
return;
}
if (req.method === "POST" && url.pathname === "/api/startup-context/file") {
const body = await readJsonBody(req);
const current = readStartupContextFile(root, body.order);
const settings = readResolvedContextRoomSettings(root, { expectedRootIdentity });
const current = readStartupContextFile(root, body.order, settings);
assertExpectedContentHash(current, body.expectedContentHash, current.path);
sendJson(res, 200, writeStartupContextFile(root, body.order, body.content, null, {
sendJson(res, 200, writeStartupContextFile(root, body.order, body.content, settings, {
expectedRootIdentity,
expectedContentHash: current.contentHash,
expectedDisplayPath: current.path,
Expand All @@ -22481,7 +22484,8 @@ async function routeRequest(req, res, root, globalPreferencesPath = null, {
}
if (req.method === "POST" && url.pathname === "/api/startup-context/delete") {
const body = await readJsonBody(req);
sendJson(res, 200, deleteStartupContextFile(root, body.order, null, { expectedRootIdentity }));
const settings = readResolvedContextRoomSettings(root, { expectedRootIdentity });
sendJson(res, 200, deleteStartupContextFile(root, body.order, settings, { expectedRootIdentity }));
return;
}
if (req.method === "GET" && url.pathname === "/api/startup-hooks/file") {
Expand Down
60 changes: 60 additions & 0 deletions test/context_room.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1994,6 +1994,66 @@ test("raw config narrowing fails closed until the human owner explicitly saves t
assert.equal(buildContextRoomDoctorReport(root).issues.some((issue) => issue.type === "review_authority_tamper"), false);
});

test("startup context API opens files from the last owner-authorized scope after raw narrowing", async (t) => {
const originalHome = process.env.HOME;
const home = makeRoot();
const root = path.join(home, "work", "project");
const hubHome = makeRoot();
const previousHubHome = process.env.CONTEXT_ROOM_HUB_HOME;
process.env.HOME = home;
process.env.CONTEXT_ROOM_HUB_HOME = hubHome;
t.after(() => {
if (originalHome === undefined) delete process.env.HOME;
else process.env.HOME = originalHome;
if (previousHubHome === undefined) delete process.env.CONTEXT_ROOM_HUB_HOME;
else process.env.CONTEXT_ROOM_HUB_HOME = previousHubHome;
});

fs.mkdirSync(path.join(home, ".codex"), { recursive: true });
fs.mkdirSync(root, { recursive: true });
fs.writeFileSync(path.join(home, ".codex", "AGENTS.md"), "# Global agent instructions\n");
initializeContextRoomProject(root, { allowedPaths: ["docs/"], watchAllow: [] });

const room = createMemoryServer({ root });
await new Promise((resolve) => room.server.listen(0, "127.0.0.1", resolve));
t.after(() => new Promise((resolve) => room.server.close(resolve)));
const baseUrl = `http://127.0.0.1:${room.server.address().port}`;
const authorized = readMemoryWebappSettings(root);
authorized.startupContext = {
enabled: true,
projectOnly: false,
fileNames: ["AGENTS.md"],
globalPaths: ["~/.codex/AGENTS.md"],
};
const authorizeResponse = await fetch(`${baseUrl}/api/settings`, {
method: "POST",
headers: { "content-type": "application/json", "x-context-room-owner-nonce": room.ownerMutationNonce },
body: JSON.stringify({ settings: authorized }),
});
assert.equal(authorizeResponse.status, 200);

const configPath = path.join(root, CONFIG_FILE);
const narrowed = JSON.parse(fs.readFileSync(configPath, "utf8"));
narrowed.startupContext.projectOnly = true;
narrowed.startupContext.globalPaths = [];
fs.writeFileSync(configPath, JSON.stringify(narrowed, null, 2) + "\n");

const queue = buildDocQaReport(root).queue;
assert.equal(queue.some((item) => item.path === "~/.codex/AGENTS.md"), true);
assert.ok(buildContextRoomDoctorReport(root).issues.some((issue) => issue.type === "review_authority_tamper"));

const listResponse = await fetch(`${baseUrl}/api/startup-context`);
assert.equal(listResponse.status, 200);
const listed = await listResponse.json();
assert.deepEqual(listed.files.map((file) => file.startupContext.displayPath), ["~/.codex/AGENTS.md"]);

const openResponse = await fetch(`${baseUrl}/api/startup-context/file?order=1`);
assert.equal(openResponse.status, 200);
const opened = await openResponse.json();
assert.equal(opened.path, "~/.codex/AGENTS.md");
assert.equal(opened.content, "# Global agent instructions\n");
});

test("direct review-state and ledger forgery is ignored and reported as critical", () => {
const root = makeRoot();
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
Expand Down
Loading