From b3fe10a4f3f6466f2bc61a439746b7728319f0f6 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Mon, 12 Oct 2026 15:07:21 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20v2.7.4=20=E2=80=94=20a=20developer's=20S?= =?UTF-8?q?ystem=20page=20carries=20no=20host=20or=20other-user=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/settings/info for a developer: no DB path, no hooks file path, and no transcript-cache keys — those were EVERY user's transcript paths (their folders and session ids). GET /api/updates/status: no repo_root. Test: server-mode-leaks. - Also in this release (earlier commits): a member's missing workspace folder is made as them at start; --allow-dangerously-skip-permissions only for an account that accepted the bypass warning or a bypass run. Full suites: server 2889/2889, client 1250/1250, tsc -b. --- CLAUDE.md | 2 +- docs/API.md | 2 +- docs/SERVER-MODE.md | 16 ++++++++++++++-- server/__tests__/server-mode-leaks.test.js | 20 ++++++++++++++++++++ server/routes/settings.js | 13 +++++++++---- server/routes/updates.js | 4 +++- 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6d09fa82..14a2814b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Working Guide -_Last synced: v2.7.4 (2026-10-12) — Changes hides a staged change undone on disk (#72); chat says when the session has not started (#73)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. +_Last synced: v2.7.4 (2026-10-12) — Changes hides a staged change undone on disk (#72); chat says when the session has not started (#73); a member's missing folder is made as them, bypass offered only once accepted._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. ## Project mission - Maintain a reliable local-first dashboard for Claude Code session monitoring. diff --git a/docs/API.md b/docs/API.md index 1f03862e..65d63f5a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -359,7 +359,7 @@ Error `409 ESESSIONLIVE` is returned when `resumeSessionId` (or `resume`) names Error `400 EBADPROMPT` is returned for a `prompt` (or `POST /api/run`'s `initialPrompt`) over 128 KB: no CLI can take one argument that long (Linux's `MAX_ARG_STRLEN`, issue #68) — attach the text as a file instead. A prompt over 4 KB never rides in tmux's argv (one tmux message holds 16 KB, `command too long`): outside Windows it is written to `/run-prompts/.txt` (0600) and the pane's own `sh` reads it back as the CLI's last argument and deletes the file. An `after-start` tool's paste still goes through `tmux set-buffer` and stays under that 16 KB. -Error `409 ENOCWD` is returned when the workspace's folder no longer exists (or is not a directory) — the pane would otherwise start in the account's home under this workspace's name. A folder this server cannot read (`EACCES`) is not refused. +Error `409 ENOCWD` is returned when the workspace's folder no longer exists (or is not a directory) — the pane would otherwise start in the account's home under this workspace's name. A folder this server cannot read (`EACCES`) is not refused. Server mode with one Linux user per account: a missing folder inside a member's own home is created instead, as that member (`mkdir -p` through `write-as`) — their home itself can be no workspace, so this is how a new member's first project folder comes to exist. Error `409 ERUNLIVE` is returned if the workspace already has a live run; stop it first with the `stop` action. The same code and status come back when the workspace's tmux pane is already running an agent while a `prompt` or `resumeSessionId` was sent — that argv cannot be typed over a live agent, so the request is refused instead of answered 200 with nothing done. A start that asks for neither still adopts the existing pane silently. diff --git a/docs/SERVER-MODE.md b/docs/SERVER-MODE.md index e759eb2d..a0475a83 100644 --- a/docs/SERVER-MODE.md +++ b/docs/SERVER-MODE.md @@ -822,8 +822,20 @@ agenthub users provision alice # the Linux account only (a user made before writes three `auth.log` lines); a pane the dashboard starts or stops shows at once, one that exits on its own within those 5 s. - Claude Code does not carry a folder's trust down from the member's home - (which provisioning trusts): a member answers "Trust this folder" once per - workspace, in the terminal. + (which provisioning trusts): a member may be asked "Trust this folder" once + per workspace, in the terminal. AgentHub never answers it for them — it is + what keeps a cloned repository's own settings and hooks from running unasked. +- A member's first project folder: a workspace named on a folder that does not + exist yet inside their home is created at its first start, AS the member + (from v2.7.4; their home itself is no workspace). +- Bypass in a pane's shift+tab cycle (`--allow-dangerously-skip-permissions`) + is offered only to an account that already accepted Claude Code's bypass + warning (`skipDangerousModePermissionPrompt` in its `~/.claude/settings.json`) + or to a run that starts in bypass itself. Before v2.7.4 every new member's + first session opened on that warning, which the Chat UI cannot answer. +- A developer's System page (`GET /api/settings/info`, `GET /api/updates/status`) + carries no path: not the database's, the hooks file's, the install folder, + nor the transcript cache's keys — those are every user's transcript paths. ### Undo and uninstall diff --git a/server/__tests__/server-mode-leaks.test.js b/server/__tests__/server-mode-leaks.test.js index 75721c76..2bc1763d 100644 --- a/server/__tests__/server-mode-leaks.test.js +++ b/server/__tests__/server-mode-leaks.test.js @@ -148,3 +148,23 @@ describe("server mode: a member's skills/commands come from their own workspaces assert.ok(names(r).includes("max-skill")); }); }); + +describe("the System page's facts for a developer", () => { + // Every string that looks like an absolute path, anywhere in a body. + const paths = (o, out = []) => { + if (typeof o === "string" && o.startsWith("/")) out.push(o); + else if (o && typeof o === "object") for (const v of Object.values(o)) paths(v, out); + return out; + }; + + it("carries no host or other-user path (DB, hooks file, transcript cache keys)", async () => { + const mia = await login("mia", "member-pass-1"); + const info = await call("/api/settings/info", mia); + assert.equal(info.status, 200); + assert.deepEqual(paths(info.body), []); + assert.equal(typeof info.body.db.size, "number", "the numbers stay"); + const boss = await login("boss", "admin-pass-1"); + const full = await call("/api/settings/info", boss); + assert.ok(full.body.db.path, "an admin still sees the host's paths"); + }); +}); diff --git a/server/routes/settings.js b/server/routes/settings.js index a4457c3d..56b26630 100644 --- a/server/routes/settings.js +++ b/server/routes/settings.js @@ -35,7 +35,7 @@ const { importExportBundle, exportableRun, } = require("../lib/data-transfer"); -const { sessionWhere, bySessionWhere } = require("../lib/scope"); +const { sessionWhere, bySessionWhere, memberId } = require("../lib/scope"); function getDbSize() { try { @@ -127,15 +127,20 @@ router.get("/info", (req, res) => { h1: getCount(60 * 60 * 1000), }; + // A developer (server mode) gets the numbers, never a path of the host's: + // the transcript cache is keyed by EVERY user's transcript path (their + // folders, their session ids), and the DB/settings paths are the host's. + const developer = memberId(req) != null; + const cache = transcriptCache.stats(); res.json({ db: { - path: DB_PATH, + path: developer ? null : DB_PATH, size: dbSize, counts, pragmas, load_stats, }, - hooks: hookStatus, + hooks: developer ? { installed: hookStatus.installed } : hookStatus, server: { version: APP_VERSION, uptime: process.uptime(), @@ -152,7 +157,7 @@ router.get("/info", (req, res) => { free_mem: os.freemem(), cpus: os.cpus().length, }, - transcript_cache: transcriptCache.stats(), + transcript_cache: developer ? { ...cache, keys: undefined } : cache, }); }); diff --git a/server/routes/updates.js b/server/routes/updates.js index 4d5afc58..86f57de1 100644 --- a/server/routes/updates.js +++ b/server/routes/updates.js @@ -24,9 +24,11 @@ const { schedulePluginUpdate } = require("../lib/plugin-update"); const router = Router(); -router.get("/status", async (_req, res) => { +router.get("/status", async (req, res) => { try { const status = await getUpdatesStatus(); + // The install folder is the host's; a developer sees versions only. + if (require("../lib/scope").memberId(req) != null) status.repo_root = null; res.json(status); } catch (err) { res.status(500).json({