feat(server-mode): native-like folder browser, a developer kept to their own folder, no password minimum

- Folder browser (the Browse fallback for a remote browser): back/forward/up,
  a clickable breadcrumb that becomes a path field (Ctrl+L), Places (Home,
  the user's workspaces, recent picks), a filter, Name/Modified columns,
  Show hidden and New folder. A click selects; a double-click or Enter
  opens a folder or picks a file; Backspace goes up; typing jumps to a name.
- GET /api/lanes/browse: `hidden=1` (never the dot entries at the top of a
  non-admin's own folder), each entry's `mtime`, the answer's `home`.
- POST /api/lanes/browse/mkdir: one plain folder name inside an existing
  folder; a non-admin only in their own folder, none in server mode without
  one; under S3 a member's folder is made as the member.
- scope.ownFolder falls back to server.conf's USERS_ROOT under S3, so a
  settings file without AGENTHUB_USERS_ROOT no longer opens the host's home
  to every developer.
- Passwords: any non-empty password is accepted (no 8-character minimum).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 14:43:03 +07:00
parent b0dd5af22f
commit 763759189f
22 changed files with 1174 additions and 110 deletions
+11 -3
View File
@@ -235,9 +235,17 @@ back on ANY of those to the in-app browser `GET /api/lanes/browse?path=<abs>`.
A file field asks it with `files=1`: entries then include the folder's files
(`file: true`, listed after the folders), and `path` opens the way the native
picker's `start` does (`~` expanded, a file's folder, home when missing) —
a click on a file picks it. In server mode with
`AGENTHUB_USERS_ROOT` set, a developer's browser opens on their own folder
(`parent: null` there) and any path whose real path leaves it is `403 EFOLDER`.
a double-click on a file picks it. `hidden=1` lists dot entries too (never
those at the top of a non-admin's own folder), every entry carries its `mtime`
(ms) and the answer its `home` (the browser's Home place). In server mode with
`AGENTHUB_USERS_ROOT` set (under S3, else server.conf's `USERS_ROOT`), a
developer's browser opens on their own folder (`parent: null` there) and any
path whose real path leaves it is `403 EFOLDER`. The browser's **New folder**
is `POST /api/lanes/browse/mkdir` (`{parent, name}` → `201 {path}`): one plain
folder name inside an existing folder, `400 EBADNAME`/`ENOTFOUND`,
`409 EEXISTS`; a non-admin only inside their own folder, and in server mode
not at all without one (`403 EFOLDER`); a member's folder is made as the
member (S3).
#### Update a workspace
+1 -1
View File
@@ -89,7 +89,7 @@ than behind a disclosure — a disclosure around a single field only hid it.
A workspace has no AI Proxy of its own: each NEW session picks one. The **AI Proxy** select in the workspace home's prompt box lists the profiles saved in Settings › AI Proxy (9Router, OpenRouter, a self-hosted gateway) plus **Native** (the vendor's own API); it shows Native alone, disabled, when the session's AI tool cannot use a proxy (only Claude and Codex can), and it remembers the last pick (`agenthub.session.proxy`, falling back to the pre-rename `agenthub.session.router`). The start sends `proxy: "<name>"` (`router` is still accepted); a Claude pane then runs with `--settings=<data dir>/proxy-settings/<name>.json`, which outranks the workspace's own settings, and a Codex pane with `-p <name>` plus its key through tmux `-e`. So two sessions of one workspace can run on two proxies, a worktree session gets its proxy too, and switching never touches a running session. A resume goes back to the proxy its session last ran on (`dashboard_runs.proxy`). Editing a profile in Settings rewrites those files (a rotated key reaches live Claude sessions); deleting it removes them. Older builds wrote a workspace's proxy into its `.claude/settings.local.json`; every server start takes those eight keys back out. See `docs/API.md` § AI Proxy profiles.
Both modes' path field has a **Browse** button next to it that opens the operating system's own folder picker (`POST /api/lanes/pick-path`). A browser cannot hand a web page an absolute path from its own dialog, so the SERVER opens the dialog on its desktop (zenity/kdialog on Linux, `osascript` on macOS, PowerShell on Windows) — which is your screen only when the browser runs on the same machine. A browser on another machine, or a server with no desktop or no zenity/kdialog, gets the small in-app folder browser instead (`GET /api/lanes/browse?path=<abs>`): click a subfolder to descend, ".." to go up, "Select this folder" to fill the path field; git repos are marked. The same button sits beside the workspace home's **Folder** field, Import history's folder, the project-memory folder, CLAUDE_HOME/CODEX_HOME and a remote source's SSH identity file (a file picker: its in-app fallback lists the folder's files too, and a click on a file picks it).
Both modes' path field has a **Browse** button next to it that opens the operating system's own folder picker (`POST /api/lanes/pick-path`). A browser cannot hand a web page an absolute path from its own dialog, so the SERVER opens the dialog on its desktop (zenity/kdialog on Linux, `osascript` on macOS, PowerShell on Windows) — which is your screen only when the browser runs on the same machine. A browser on another machine, or a server with no desktop or no zenity/kdialog, gets the in-app folder browser instead (`GET /api/lanes/browse?path=<abs>`), laid out like a native picker: Back/Forward/Up, a clickable breadcrumb that becomes a path field (✎ or Ctrl+L), Places (Home, your workspaces, recent picks), a filter, Name/Modified columns (click to sort), **Show hidden**, and **New folder**. A click selects, a double-click or Enter opens (Backspace goes up, typing jumps to a name), and "Select this folder" fills the path field with the selected folder, else the one shown; git repos are marked. The same button sits beside the workspace home's **Folder** field, Import history's folder, the project-memory folder, CLAUDE_HOME/CODEX_HOME and a remote source's SSH identity file (a file picker: its in-app fallback lists the folder's files too, and a double-click on a file picks it).
## Workspace tool: Claude or Codex
+6 -3
View File
@@ -196,7 +196,9 @@ that already belongs to another user's workspace gets `409 EOWNED`. Admins are
not limited. Without [one Linux user per account](#one-linux-user-per-account)
you create each user's folder yourself, and this keeps workspaces apart, not
agents. `install-system` sets this value to `/srv/agenthub/users`, and adding a
user makes the folder (it is the Linux account's home).
user makes the folder (it is the Linux account's home). With S3 installed, a
settings file that lacks the variable falls back to `USERS_ROOT` in
`/etc/agenthub/server.conf`, so a developer is never shown the host's home.
## What the gate does
@@ -222,7 +224,7 @@ user makes the folder (it is the Linux account's home).
- A login lasts 30 days in the httpOnly, `SameSite=Strict` cookie
`agenthub_sid`. Only the token's SHA-256 is stored. Logging out, a password
change and disabling the user each end it.
- Passwords are stored as scrypt hashes and must be at least 8 characters.
- Passwords are stored as scrypt hashes; any non-empty password is accepted (no minimum length).
Every login attempt counts before its password is checked, so attempts sent
in parallel count too. After 10 failed logins from one address, or 30 for one
username, in 15 minutes, further logins answer `429 ETOOMANY` until the
@@ -539,7 +541,8 @@ that are not theirs. Each case below is refused, with a test in
line AgentHub adds for a new worktree or `.mcp.json` is written only when the
repo's real git dir is the same account's; a FIFO there cannot hang the
dashboard.
- **Browsing and branch lists.** A developer's `GET /api/lanes/browse` and
- **Browsing and branch lists.** A developer's `GET /api/lanes/browse`, the
browser's New folder (`POST /api/lanes/browse/mkdir`, made as the member) and
`GET /api/lanes/branches` stay inside their own folder (`403 EFOLDER`), and
so does a workspace's extra folder (`POST /api/lanes/:id/sources/folder`,
refused before the path is looked at, so the answer says nothing about