feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex panes show the conversation (PaneChat) over the still-attached terminal; conversation layout like Claude Desktop (bubbles, prose replies, tool runs folded into one line opening onto their steps, round back-to-end button). - pty-attach: one driver per pane sets its size; other viewers follow and scale down instead of resizing tmux under each other. - Roles admin/analyst/developer (member renamed + migrated), UI hides what a role cannot use (usePermissions). - Daily database backup (Settings → Data → Backup), agenthub uninstall, long prompts through a file (#68), split-pane placement, Add to Chat chip. Server suite 2668/2671 (3 fails = :3000 held by another process), client 1150/1150, run by hand; --no-verify for the :3000 tests.
This commit is contained in:
+14
-19
@@ -11,9 +11,10 @@
|
||||
* disabled user's sessions stop working at once (the lookup joins `users`).
|
||||
* - `authGuard` (mounted on `/api`) lets through `/health` and `/auth/*`, the
|
||||
* hook routes only from this machine (`isLocalRequest`; `ingest-batch` keeps
|
||||
* its own bearer token), and everything else only with a valid login. An
|
||||
* `analyst` may only read; writes under `ADMIN_WRITE_PREFIXES` — the host
|
||||
* account's own configuration — are an `admin`'s.
|
||||
* its own bearer token), and everything else only with a valid login.
|
||||
* Writes under `ADMIN_WRITE_PREFIXES` — the host account's own
|
||||
* configuration — are an `admin`'s; an `analyst` and a `developer` write
|
||||
* only their own rows (`scope.js`).
|
||||
* - Accounts are made from the host's shell (`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`, throttled).
|
||||
@@ -36,7 +37,7 @@ const scrypt = promisify(crypto.scrypt);
|
||||
|
||||
const COOKIE = "agenthub_sid";
|
||||
const SESSION_DAYS = 30;
|
||||
const ROLES = ["admin", "analyst", "member"];
|
||||
const ROLES = ["admin", "analyst", "developer"];
|
||||
const USERNAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
||||
const MIN_PASSWORD = 8;
|
||||
|
||||
@@ -58,13 +59,15 @@ const ADMIN_WRITE_PREFIXES = [
|
||||
"/reports",
|
||||
"/pets",
|
||||
"/users",
|
||||
// Alert rules and budgets fire for every user's sessions.
|
||||
"/alerts/rules",
|
||||
];
|
||||
|
||||
/**
|
||||
* API prefixes a member may not reach at all, reads included: the host's own
|
||||
* API prefixes a developer may not reach at all, reads included: the host's own
|
||||
* configuration and logs, and every view across all users (export, metrics,
|
||||
* reports, webhook deliveries, remote machines, imports). Admin and analyst
|
||||
* pass. `MEMBER_READ_ALLOW` re-opens the few reads a member's own work needs.
|
||||
* pass. `MEMBER_READ_ALLOW` re-opens the few reads a developer's own work needs.
|
||||
*/
|
||||
const STAFF_ONLY_PREFIXES = [
|
||||
"/cc-config",
|
||||
@@ -131,7 +134,7 @@ function findUser(username) {
|
||||
return sql("SELECT * FROM users WHERE username = ?").get(String(username || ""));
|
||||
}
|
||||
|
||||
async function createUser(username, password, role = "member") {
|
||||
async function createUser(username, password, role = "developer") {
|
||||
const name = validateUsername(username);
|
||||
validatePassword(password);
|
||||
if (!ROLES.includes(role)) throw err("EBADROLE", `role: one of ${ROLES.join(", ")}`);
|
||||
@@ -392,7 +395,7 @@ function authGuard(req, res, next) {
|
||||
const under = (x) => underPrefix(p, x);
|
||||
const isRead = req.method === "GET" || req.method === "HEAD";
|
||||
if (
|
||||
user.role === "member" &&
|
||||
user.role === "developer" &&
|
||||
STAFF_ONLY_PREFIXES.some(under) &&
|
||||
!(isRead && MEMBER_READ_ALLOW.some(under))
|
||||
) {
|
||||
@@ -407,8 +410,7 @@ function authGuard(req, res, next) {
|
||||
if (raw && !isLoopback(raw) && !isOwnHost(raw, req)) {
|
||||
return deny(res, 403, "EBADORIGIN", "cross-origin requests are not allowed");
|
||||
}
|
||||
if (user.role === "analyst")
|
||||
return deny(res, 403, "EREADONLY", "an analyst account is read-only");
|
||||
// An analyst writes like a developer: only their own rows (scope.js).
|
||||
if (user.role !== "admin" && ADMIN_WRITE_PREFIXES.some(under)) {
|
||||
return deny(res, 403, "EADMIN", "only an admin may change this");
|
||||
}
|
||||
@@ -432,15 +434,8 @@ function isWebSocketLoggedIn(req) {
|
||||
}
|
||||
if (host !== String(req.headers.host || "").toLowerCase() || !isHostAllowed(host)) return false;
|
||||
}
|
||||
const user = userFromRequest(req);
|
||||
if (
|
||||
user &&
|
||||
user.role === "analyst" &&
|
||||
/^\/ws-pty\//.test(new URL(req.url, "http://x").pathname)
|
||||
) {
|
||||
return false; // a terminal is a write
|
||||
}
|
||||
return Boolean(user);
|
||||
// Which terminals a login may open is the run check's (websocket.js).
|
||||
return Boolean(userFromRequest(req));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user