feat: v2.7.6 — edit users in one form, Settings → Account, display names, copy over plain HTTP
- Users page: one Edit user form (display name, email, role, active, a new or generated password, workspaces given or taken back, sign out everywhere), validated as a whole before anything is written (PATCH /api/users/:id, GET /api/users/:id); users.display_name/email added migration-safe. - Settings → Account for every signed-in user: profile, own password (the current one required, this login kept), sign out other devices (/api/me/account, /api/me/password, /api/me/sign-out-others; login cookie only, never a pane token). - Display names wherever a person is named (sidebar, workspace owner groups, Users page and audit log); owner_display_name / reader_display additive. - Users page: admins add and remove Claude/Codex pool accounts. - Folder browser: an admin with their own Linux account opens in their home; the "Server folder" tick opens the host's (never remembered). - AI Proxy: "Config 9router" beside Add profile when 9router is installed, opened on the dashboard's own host (GET /api/ai-proxies/9router). - Terminal: a tmux selection reaches the clipboard over plain HTTP too (writeClipboard's textarea fallback, focus handed back). - Chat: a file the session touched outside the workspace previews (GET /api/sessions/:id/file). - install-system: `!requiretty` only when visudo accepts it (sudo-rs on Ubuntu 25.10+ rejects the setting). - openapi.yaml regenerated; docs (API, SERVER-MODE, LANES) updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Claude Code Working Guide
|
||||
|
||||
_Last synced: v2.7.5 (2026-10-13) — the chat shows each AskUserQuestion with the user's answer; while iterating, run only the tests a change touches._ 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.6 (2026-10-13) — an admin adds and removes Claude/Codex pool accounts on the Users page._ 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.
|
||||
@@ -78,7 +78,7 @@ See `docs/LANES.md` for full guide: stage reporting, custom templates, lane acti
|
||||
- **A headless entry point requires `server/lib/windows-hide.js` before anything reads `child_process`.** The dashboard and its detached helpers have no console on Windows, so every git/npm call without `windowsHide` opened a console window that flashed and stole focus on each poll. `install()` (in `server/index.js`, `plugin-bootstrap.js`, `plugin-update-helper.js`, `cc-update-all-helper.js`, `restart-helper.js`) forces it for every spawn; a new detached script needs the same first line, or a `windowsHide: true` of its own (the statusline's one git call has that).
|
||||
- **Server mode is a MODE of this codebase, never a fork, and local mode must not notice it.** Everything server-only checks `isServerMode()` (`AGENTHUB_MODE=server`, `server/lib/auth.js`) and is a no-op otherwise; `auth.js` opens the database only on first use, so a module that merely loads it never touches the DB in local mode. Accounts are made from the host's shell (`agenthub server setup`, `agenthub users …`) or by an admin on the Users page (`server/routes/users.js`), where EVERY change needs the admin's own password again (`confirmPassword` in `auth.js`, throttled — EREAUTH/ETOOMANY), never lets an admin demote or disable themselves (ESELF), is written to `audit_log`, and refuses in local mode (ELOCALMODE: no login to check) — do not add an account route without that step-up, and never one that returns a password hash. Rows are OWNED (phase S2, `server/lib/scope.js`): `lanes.owner_id` is the creator (never from the request — `ownerFor(req)`), a session belongs to its workspace (`sessions.lane_id` or a `dashboard_runs` row), and everything else hangs off the session. A `developer` sees only their own rows, and an `analyst` reads every row but writes only their own (`memberId` scopes an analyst's non-GET requests like a developer's — `member` was renamed `developer`, `db.js` migrates it): every list/aggregate passes `req` into `sessionWhere`/`bySessionWhere`/`laneWhere`; every route naming ONE row is covered by `scopeGuard`'s `PATH_RULES` + the query/body id names in `namedIds` (404 `ENOTFOUND`) — a new id route under a new prefix needs a rule there; every WebSocket broadcast type needs a `broadcastAudience` case (an unlisted type reaches no developer); a staff-wide view joins `STAFF_ONLY_PREFIXES`. Admin/analyst reads of a developer's session are written to `audit_log` (`server/lib/audit.js`). A pane carries `AGENTHUB_PANE_TOKEN` (hash in `dashboard_runs.pane_token_hash`, never listed) so the CLI/MCP/statusline inside it act as the workspace owner — accepted only from loopback and only while the pane lives (`userFromPaneToken`). Every write in server mode passes the Origin check in `authGuard`; prefix checks go through `apiPath()` (decoded, case-insensitive), because Express routes `/api/Webhooks` and `%77ebhooks` too. Named locks are namespaced per user, and `named-lock.js` takes only a plain name (a lock name was a path). It is NOT OS isolation unless S3 is installed (next rule); without it every pane runs as the host account, so do not call that multi-tenant. A new route that writes the host's own configuration joins `ADMIN_WRITE_PREFIXES`, and its controls hide behind `usePermissions().isAdmin` (`client/src/components/AuthGate.tsx`) — a hidden button is a courtesy, the server's refusal is the rule; hooks stay loopback-only in server mode.
|
||||
- **With S3 installed (server mode + `/etc/agenthub/server.conf`, `s3Enabled`), a pane runs as its workspace owner's Linux user, and the dashboard reaches a member only through sudo.** `ah-<name>`, home `<USERS_ROOT>/<name>`; the FOLDER decides (`accountForPath` in `server/lib/run-as.js`), and a folder under the users root with no enabled, provisioned user is `EMEMBERGONE` — never fall back to the host there. Every tmux call on a member's socket (attach included), every git/`gh` call in a member's folder and every write is `sudo -n -u ah-<name> <tmux|git|gh|write-as>` by the absolute paths in `server.conf` (`sudoArgv`); sudoers allows exactly those, plus `/opt/agenthub/tools/bin/claude` for the opt-in plugin baseline (`applyPluginBaseline`), as `%agenthub-members` and NEVER root as a run-as target. **The only root commands are `/opt/agenthub/sbin/agenthub-provision` (create/disable/enable — it never deletes, never adopts a non-member account, and root never touches a path inside an existing home) and `systemctl start|stop|restart|enable|disable agenthub.service`;** do not add a third — a host compromise is already every member's. **sudo logs argv, so nothing secret or private crosses it:** a member pane's env (pane token, pool token) and command (prompt) go in a launch file written by `write-as` from stdin and run by `pane-launch`; file content always goes on stdin. **The host never writes, chmods or deletes inside a member's home itself — only through `write-as` as the member** (`writeFileAs`, `handoffWrite`, `mkdirAs`, `removeAs`, `scratchFile`): the member can swap any name there for a link, and the host's write would follow it. A member's file is READ only through `openSafe`/`readFileSafe*`/`readStreamSafe` (open, then `fstat`: a regular file that member owns, `O_NONBLOCK`), never a plain `fs` read. **Hooks are authenticated by the peer uid** of the loopback connection (`/proc/net/tcp*`, `server/lib/hook-auth.js`), never by anything in the body: host or root = trusted, an enabled `ah-*` = only work, sessions, runs and transcript paths inside its own home (`EFOREIGN`, foreign paths dropped BEFORE any tool mapping reads them), anyone else `EPEER`. **`hidepid=invisible` (gid `agenthub-proc` exempt) is required** — panes' argv/env would otherwise be readable by every account — and `install-system` stops without it. The unit's ambient `CAP_DAC_READ_SEARCH` is dropped for every child (`server/lib/drop-caps.js`, installed first in `server/index.js`, before anything reads `child_process`) — keep it there, and a spawn outside `child_process` (node-pty) takes `ptyArgv`. **The account comes from a path's TEXT, so a path a member can redirect is checked before it is used:** a workspace's real path (`laneRoot` in `git-diff.js`, `EFOREIGN`) and a git dir their git prints (`git-exclude.js`) must stay the same account's (`sameOwner`), a member's home is no workspace, and a READ uses `ownerForRead`, never the throwing `accountForPath`. **A workspace inside a member's home is owned by that member only, and one outside the users root (it runs as the HOST) by an admin only, by the LIVE role** (`assertOwnerFits`: create, assign, every pane start — `EOWNERFOLDER`; demoting or disabling a user ends their host panes): its panes run as the FOLDER's account but carry the OWNER's pane token, which that account can read; a pane's cwd must be that same account's (`ECWDACCOUNT`), and a pane token is accepted only from that account's uid (`peerIsPaneAccount`). A path a non-admin names in ANY request (an analyst's GET too) stays in their own folder or workspaces (`ownFolder`, `insideOwnWorkspace`, `scope.ownCwdQuery`), and the config `file` reads never serve a credential file or anything in the data dir (`isSecretConfigPath`). **A member can stall or flood what the dashboard reads, and it all runs on one thread:** every synchronous sudo into a member is time-bounded (`MEMBER_EXEC`, tmux `memberCall` with a back-off) and a member's tmux failure makes only THEIR panes unknown, never everyone's; a member's file is read with a size cap (`readFileSafe*` `max`), line by line only through `readLines` (never `readline`, which a one-line 512 MB file crashes), and a sparse one is never copied out as real blocks. Operator steps: `docs/SERVER-MODE.md` § Operator runbook.
|
||||
- **A pool token reaches a pane only through tmux `-e` (a member's pane under S3: its launch file), and only for the account `pickAccount` chose.** Never type it into a pane, never put it in the CLI's argv, never return it from a route (`poolStatus` has no token field), and never add a route that adds or reads one — the pool is filled from the host's shell (`agenthub accounts add [--tool codex]`). An empty pool must leave every spawn exactly as before. The ONE exception is a Codex ChatGPT login (`server/lib/codex-pool.js`), which Codex reads from no env var: it is written as `auth.json` into a MEMBER's own CODEX_HOME, as the member (`write-as`, 0600), never into a host pane's CODEX_HOME, never over a login AgentHub did not place, and never a second account into a home where a live pane of that member runs on one; `syncBack` copies the newest back. A Codex API key is `-e AGENTHUB_POOL_KEY` plus a `-c` provider whose `env_key` only names it.
|
||||
- **A pool token reaches a pane only through tmux `-e` (a member's pane under S3: its launch file), and only for the account `pickAccount` chose.** Never type it into a pane, never put it in the CLI's argv, never return it from a route (`poolStatus` has no token field), and never add a route that reads one back — the pool is filled from the host's shell (`agenthub accounts add [--tool codex]`) or by an admin on the Users page (`POST`/`DELETE /api/accounts`, `/accounts` in `ADMIN_WRITE_PREFIXES`, their own password again, audited by account NAME), where the token only goes in. An empty pool must leave every spawn exactly as before. The two exceptions are a Claude login (`server/lib/claude-pool.js` kind `login`, moved in from a saved switch-user profile — the host CLI's own moves with its live login, and the host's home becomes one more placement — or a pasted `.credentials.json`) and a Codex ChatGPT login (`server/lib/codex-pool.js`), which neither CLI reads from an env var: it is written as `auth.json` into a MEMBER's own CODEX_HOME, as the member (`write-as`, 0600), never into a host pane's CODEX_HOME, never over a login AgentHub did not place, and never a second account into a home where a live pane of that member runs on one; `syncBack` copies the newest back. A Claude login's refresh token is single use, so the server is its one refresher (`refreshLogins`, ahead of expiry, pushing the new pair into every home it placed it in), and a pair a member's Claude refreshed first is copied back only once Anthropic confirms the same account (`fetchOwner`). A Codex API key is `-e AGENTHUB_POOL_KEY` plus a `-c` provider whose `env_key` only names it.
|
||||
- Preserve existing behavior unless explicitly asked to change it.
|
||||
- Prefer minimal, reversible diffs.
|
||||
- Never silently weaken safety controls around destructive actions.
|
||||
@@ -94,9 +94,9 @@ See `docs/LANES.md` for full guide: stage reporting, custom templates, lane acti
|
||||
- CLI (after setup): `agenthub <command>` — terminal access to the full dashboard surface (`bin/agenthub.js`; `agenthub help` lists commands)
|
||||
|
||||
## Testing and verification policy
|
||||
- Backend changes: run `npm run test:server` before finishing.
|
||||
- Frontend changes: run `npm run test:client` when relevant. This includes per-screen render snapshots (`client/src/pages/__tests__/screens.snapshot.test.tsx`). If a UI change is intentional, review the snapshot diff and regenerate baselines with `cd client && npx vitest run -u`; never blindly update snapshots to make tests pass.
|
||||
- Full suites take 5–6 minutes — while iterating run only what the change touches (`npm run test:server -- server/__tests__/<name>.test.js`; in `client/` `NODE_ENV=test npx vitest related <changed files> --run`), then the full suite once before committing, and always when touching something every screen uses (`Layout`, `Sidebar`, `StatusBar`, `tailwind.config.js`, global CSS, shared hooks/lib).
|
||||
- **Run only the tests the change touches — never a full suite by default.** Full suites take 5–6 minutes. Backend: `npm run test:server -- server/__tests__/<name>.test.js` (the test files for, or naming, each module you changed). Frontend: in `client/`, `NODE_ENV=test npx vitest related <changed files> --run`. Docs-only changes run no tests.
|
||||
- Run a full suite (`npm run test:server` / `npm run test:client`) ONLY when it is genuinely needed: the change touches something every screen uses (`Layout`, `Sidebar`, `StatusBar`, `tailwind.config.js`, global CSS, shared hooks/lib — the per-screen snapshots `client/src/pages/__tests__/screens.snapshot.test.tsx` catch that), a shared server core (`server/db.js`, `server/index.js`, `server/lib/auth.js`/`scope.js`, middleware), a dependency or build/test config, or more than ~10 code files; or the user asks for it. Do not re-run a full suite "once more before committing" — the pre-commit hook (`.husky/pre-commit`) already runs the related tests for a small commit and both full suites for a big or risky one.
|
||||
- If a UI change is intentional, review the snapshot diff and regenerate baselines with `cd client && npx vitest run -u <test file>`; never blindly update snapshots to make tests pass.
|
||||
- If you cannot run a verification step, state exactly what was not run and why.
|
||||
|
||||
## Change guidelines by area
|
||||
|
||||
Reference in New Issue
Block a user