From 118eaee2ecac4a82e7834772887091f72d51a033 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 9 Oct 2026 15:18:57 +0700 Subject: [PATCH] docs(server-mode): plan moved in from the deleted AgentHub-Server repo; P6 spike result; Express patch only on 4.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/plans/server-mode/README.md: the original multi-user design, now kept here (AgentHub is the one source); desktop-parity and S3-PLAN link to it. - S3-PLAN P6: managed-settings spike run on agh-test1 — the managed marketplace/plugin arrive at the first interactive session (onboarding seeded), never via the CLI alone, and no user-scope record is written. - server/index.js: the Layer.handle_request patch applies only on Express 4 (Express 5 handles async rejections itself and has no lib/router/layer). --- docs/plans/desktop-parity/README.md | 4 +- docs/plans/server-mode/README.md | 98 +++++++++++++++++++++++++++++ docs/plans/server-mode/S3-PLAN.md | 10 ++- server/index.js | 5 +- 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 docs/plans/server-mode/README.md diff --git a/docs/plans/desktop-parity/README.md b/docs/plans/desktop-parity/README.md index aa06401c..0b6f649e 100644 --- a/docs/plans/desktop-parity/README.md +++ b/docs/plans/desktop-parity/README.md @@ -2,8 +2,8 @@ _Status: written 2026-10-02. Phases 1–5 and 7 implemented and committed (a6bd33b); phase 6 implemented 2026-10-02 the Claude way (D4). Phases run in order, each one change-set on `main`. -Comes BEFORE the multi-user server plan (now in the AgentHub-Server repo, -https://git.smartgift.io.vn/Smartgift-AI/AgentHub-Server) — that plan's owner column hangs +Comes BEFORE the multi-user server plan ([`docs/plans/server-mode/README.md`](../server-mode/README.md)) +— that plan's owner column hangs off the session ownership built in phase 1 here._ ## Where we stand diff --git a/docs/plans/server-mode/README.md b/docs/plans/server-mode/README.md new file mode 100644 index 00000000..b4c81290 --- /dev/null +++ b/docs/plans/server-mode/README.md @@ -0,0 +1,98 @@ +# Multi-user server — AgentHub as a shared AI-tools host + +_Status: the original design, written 2026-10-02. It lived for a while in a +separate AgentHub-Server repo, which has since been deleted; since 2026-10-09 +it is kept here again, and AgentHub is the only source. Kept as the design +record. For what was built and what is still open, see +[`S3-PLAN.md`](S3-PLAN.md) and [`CHECKLIST.md`](CHECKLIST.md). The operator +runbook is in [`docs/SERVER-MODE.md`](../../SERVER-MODE.md)._ + +> Two things changed after this design was written: +> - The `member` role is now `developer`. +> - An `analyst` is no longer read-only. An analyst reads every user's rows and +> writes only their own. + +## Where the code lives (decided 2026-10-02) + +- **No fork while the local AgentHub is still moving.** A fork would need every + local fix carried over by hand, and S1–S3 touch nearly every route, the DB and + `pty-run.js` — two lines editing those in parallel conflict on every sync. +- **Server features go into the AgentHub codebase as a server MODE**, off by + default: login, roles and per-user scoping are additive; with the mode off the + local app behaves as one implicit admin and nothing changes for it. +- **What only a server deployment needs ships in this repo too**: + - provisioning, the sudoers rule and the pane helper are in + `scripts/server/`, installed by `agenthub server install-system`; + - the shared tools layout is `/opt/agenthub/tools`; + - the operator docs are the runbook in `docs/SERVER-MODE.md`. + (This first went to a separate AgentHub-Server repo. That repo is gone.) +- If a separate codebase ever becomes necessary, fork from a tagged, stable + AgentHub release, never from work in progress. + +## Goal + +Several people (A, B, C) use one AgentHub over the web. One Linux account (the +**host account**) has installed every AI tool and holds the shared logins: +AI Router profiles, several Claude native accounts, several Codex accounts. + +- Each person logs in to AgentHub, works only in their own folder, and can + switch Claude/Codex account, pick any AI Router and use any installed tool — + without installing a tool or logging in to one themselves. +- Nobody sees anyone else's workspaces, sessions, prompts, tokens or cost. +- An **admin** sees and manages everything; an **analyst** reads everything + (usage, prompts) but changes nothing. + +## Why one Linux user per account + +Isolation inside AgentHub alone covers only the web UI. An agent runs a shell +(and can be put in bypass mode) as whatever Linux user started it; if A's agent +runs as the same user as B's, "read /srv/agenthub/users/B" just works. Only the +OS can stop an agent, so each AgentHub account maps to its own Linux user, and +the folders are `0750` with the AgentHub service in the group. + +The cheaper variant — one Linux user, isolation in the UI only — is phase S1-S2 +on their own. It is acceptable only where everyone is trusted and "cannot see" +means "is not shown". + +## What is shared and what is not + +| Thing | Shared? | Where | +| --- | --- | --- | +| Tool binaries | **Yes**, installed once | `/opt/agenthub/tools/` (root or `agenthub` owned, world `r-x`), on every pane's `PATH`. Self-updaters off (`DISABLE_AUTOUPDATER=1` for Claude; per tool, verified in S0); AgentHub's **Update** runs as the owner of `/opt/agenthub/tools`. | +| Today's install | — | `~/.local/bin/claude`, `~/.codex/packages/…/codex`, `~/.local/bin/agy` inside a `0750` home: other users cannot reach them, and Codex's binary lives in its credentials dir. Hence the move. | +| Tool config / login / transcripts | **No**, per account | `/srv/agenthub/users//.claude`, `.codex`, `.gemini`, `.cursor`… (`HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` point here). Sharing this dir would share every transcript. | +| Account logins (Claude, Codex, …) | **Yes**, as a vault | Host account, `agenthub`-owned, `0700`. Injected into a pane at spawn (S5). | +| AI Router profiles | **Yes** | Already per workspace (`router-apply.js`); the store moves to the vault dir. | +| Hooks | **Yes**, one copy | Claude: `/etc/claude-code/managed-settings.json` (applies to every user). Codex: written into each account's `CODEX_HOME` at provisioning. | +| Database | One SQLite, rows owned | `owner` on every user-data table (S2). | + +## Phases + +| # | Scope | +| --- | --- | +| S0 | **Spike** (needs `sudo`): two test Linux users; tools under `/opt/agenthub/tools`; each tool started with a foreign `HOME`; verify per tool how a login can be handed in (below) and how its auto-update is turned off; measure what concurrent use of one OAuth login does. Output: a table in this file, go/no-go per tool. | +| S1 | **Accounts and login**, still one Linux user. `users` table (id, name, role `admin|analyst|member`, password hash via `crypto.scrypt`, disabled), `auth_sessions` (httpOnly SameSite=Strict cookie), login page, every `/api` and `/ws` behind it except `/api/hooks` (local only, see S3). First run: create the admin from the CLI (`agenthub users add --admin`). | +| S2 | **Ownership.** `owner` (user id) on `lanes`, `sessions`, `token_usage`, `events`, `dashboard_runs`, `lane_sources`, alerts, ai-tools prefs. A session's owner = its workspace's owner (phase-1 `lane_id`), else derived from its cwd under `/srv/agenthub/users//`. Every query of a member is scoped (one helper, not per route); WebSocket broadcasts filtered per connection. Workspace creation refuses a folder outside the account root. Admin/analyst bypass the scope; analyst is read-only. | +| S3 | **Per-account Linux user.** Provisioning (`agenthub users add`): `useradd`, `/srv/agenthub/users/` `:agenthub 0750`, `HOME` there, per-tool dirs, Codex hooks. Spawning as the account: a small privileged helper (or a `sudoers` rule limited to `tmux` under group `agenthub-users`) — per-account tmux server; `pty-run.js`/`tmux.js`/`pty-attach.js` gain a target user. Hooks keep posting to localhost; the server trusts the owner it derives from the cwd/run, never one the hook names. Transcripts read through group `agenthub`. **Security review before merge.** | +| S4 | **Shared tools.** Catalog detection also looks in `/opt/agenthub/tools/bin`; AI Tools install/update write there; per-tool auto-update off in each pane's env. | +| S5 | **Login vault and switching.** A member picks, per run or as a default, a Claude profile / Codex profile / AI Router. At spawn the pane gets it: Claude a long-lived token (`claude setup-token` → `CLAUDE_CODE_OAUTH_TOKEN` through tmux `-e`), Cursor `CURSOR_API_KEY`, Codex `auth.json` copied into the account's `CODEX_HOME` (or an API key), others per S0. Today's `cc-profiles.js` switch rewrites ONE live credentials file — that becomes "copy into this account", never global. Codex profiles are new. Refresh-token rotation (two people on one OAuth login) is the open risk S0 measures. | +| S6 | **Admin and analyst views.** User management; every page gets a user filter for admin/analyst; prompt analytics (UserPromptSubmit events, transcripts) and token/cost per user; admin may attach read-only to any pane. Every admin read of another user's content is audit-logged. | +| S7 | **Hardening.** TLS (reverse proxy), bind address, login rate limit, cookie flags, `sameOriginGuard` kept, `/api/hooks` accepted from loopback only, backup of the vault. | + +## Rules this plan will change in `CLAUDE.md` + +- Settings/`CLAUDE_HOME` become per account; the host account's `~/.claude` is no + longer "the" Claude home. +- `cc-profiles.js` "a switch changes the live account" → per account. +- Every new route is owner-scoped; "a route never takes X from the request" + rules gain "…nor an owner". +- The bootstrap/systemd rules stay for the host account; members never run the + bootstrap. + +## Open questions for the user (before S3) + +1. With Claude Enterprise each person has a seat — should members log in their + own seat (cleaner audit, no rotation risk) and the vault serve only Codex, + AI Routers and spares? +2. Does an admin's live view of a pane need to be announced to the member? +3. Retention of prompts for analytics — how long, and who may export them? diff --git a/docs/plans/server-mode/S3-PLAN.md b/docs/plans/server-mode/S3-PLAN.md index 7f6c1a01..8ab12fc9 100644 --- a/docs/plans/server-mode/S3-PLAN.md +++ b/docs/plans/server-mode/S3-PLAN.md @@ -196,7 +196,13 @@ Branch: `server-s3`. Commits happen only when the user asks. ### P6. Skills and plugins - [x] Seed a new member's `~/.claude.json`: `hasCompletedOnboarding`, theme, and trust for their home (live finding 2026-10-08). The Bypass Permissions consent stays the member's own. - Done: `agenthub-provision create` seeds onboarding + trust of the home (merged; a non-JSON file is left alone); the theme follows AgentHub's dark/light mode (`member-claude.setTheme`). Test: `server-s3-provision.test.js` runs the real script with stub `id`/`useradd`/`chown`/`runuser` and its fixed paths pointed at a temp dir. -- [ ] Spike: does `/etc/claude-code/managed-settings.json` (`enabledPlugins` plus marketplaces) give a new Linux user the admin's baseline plugins? (Script ready, NOT run — see "P6 spike: how to run it" below.) +- [x] Spike: does `/etc/claude-code/managed-settings.json` (`enabledPlugins` plus marketplaces) give a new Linux user the admin's baseline plugins? **Yes, from the first interactive session on.** Run 2026-10-09 on `agh-test1` (not a new user), Claude Code 2.1.293, with `sg-all@sg` (no hooks, public git marketplace) in a ~80 s window. The file was then restored byte for byte, and `agh-test1`'s `~/.claude` was restored from a tarball and verified. + - Before any session: `claude plugin marketplace list` and `plugin list` were both `[]`. The CLI alone syncs nothing. + - First session, onboarding NOT seeded: it stopped at the theme picker and synced nothing. The provision seed (P6, above) is a precondition. + - Onboarding seeded and NOT logged in: within seconds the session cloned the `sg` marketplace, fetched `sg-all` 0.2.0 into `~/.claude/plugins/cache/` and marked it `.in_use`. + - `installed_plugins.json` stayed empty, so `claude plugin list` shows `[]` even then. A managed-enabled plugin is loaded from the cache with no user-scope record. Check a member's plugins by the cache, not by `plugin list`. + - So: install-system can put the baseline in managed settings for anything a marketplace can express. The `plugin-baseline.json` fallback stays for the rest, and it applies to every Claude Code on the machine, the host's included (enabling a plugin the host already has changes nothing for it). + - Not checked: skills actually listed inside a logged-in session (no login on the test user). - [x] Fallback (built, opt-in): with `/etc/agenthub/plugin-baseline.json` (`{"marketplaces": ["owner/repo", …], "plugins": ["name@marketplace", …]}`, root's, the admin edits it), creating a user runs `claude plugin marketplace add ` for each marketplace, then `claude plugin install --scope user ` for each plugin, AS the new member (`sudo -n -u ah- HOME=… /opt/agenthub/tools/bin/claude …`, in the background; `agenthub users provision` runs it too, in the foreground, and is the retry). No file = nothing runs. An entry starting with `-` is skipped; `-y` is never passed. Sudoers: `/opt/agenthub/tools/bin/claude` joins the run-as-member list (no new power: the host already runs any command as a member through tmux). `server/lib/member-claude.js`, test `server-s3-provision.test.js` (fake sudo). Not verified live. - [ ] A member's own plugins work from their terminal (`/plugin`) as soon as P3 lands, with no code. - [ ] Later, optional: Settings for members, using `cc-config` with `home` = `req.user`'s and writes via `sudo -u`. @@ -283,7 +289,7 @@ Review ran 2026-10-08 (four lenses: root/sudo, cross-member, secrets, local-mode - [x] Update (2026-10-09, branch `s3-docs`): - `docs/SERVER-MODE.md`: an S3 section and a runbook; - CLAUDE.md rules: a pane runs as its owner; the dashboard never uses sudo to root except the provision helper; hooks are authenticated by peer uid; `hidepid` is required; - - the AgentHub-Server operator docs. + - the design plan moved back from the deleted AgentHub-Server repo into [`README.md`](README.md); the operator docs are the runbook in `docs/SERVER-MODE.md`. ## Known risks - `sudo` costs about 10 ms per git call. That is fine: an open Changes panel makes 5 calls every 30 s. diff --git a/server/index.js b/server/index.js index adc373b9..3f3e22bc 100644 --- a/server/index.js +++ b/server/index.js @@ -58,8 +58,9 @@ const express = require("express"); // throw after the first await) became an unhandled rejection, and Node exits // on one — any request that made a handler throw took the whole server down. // Every handler's rejection goes to `next(err)` (a 500) instead, the way a -// synchronous throw already did. Express 5 does this itself. -{ +// synchronous throw already did. Express 5 does this itself, and has no +// `lib/router/layer` to patch — so only a 4.x gets it. +if (require("express/package.json").version.split(".")[0] === "4") { const Layer = require("express/lib/router/layer"); Layer.prototype.handle_request = function handle(req, res, next) { const fn = this.handle;