feat: v2.7.7 — File Explorer on workspaces and the user's home, clone with per-user git identity

- File Explorer (Files in the sidebar, /files; also from a workspace's Files
  tab, portalled so no panel clips it), laid out like GNOME Files: places
  (Home, workspaces, Trash), Back/Forward/Up, path bar, search, icon grid with
  image thumbnails or list, labelled New/Upload, selection action bar, hidden
  files behind Ctrl+H, right-click menus and the usual keys.
  Make, rename, move, copy (within a workspace, between workspaces, home ↔
  workspace), delete into a per-account trash restorable for 7 days, streamed
  upload of files and whole folders (≤ 500 MB a batch, Replace / Keep both /
  Skip on a taken name), download a file or a zip of anything.
  server/lib/file-explorer.js, /api/lanes/:id/fs/*, /api/homes/:who/fs/*.
- The user's home: every dot entry at its top stays hidden and refused
  (also via a symlink, download, zip or as a copy source); an admin may view
  other users' homes read-only, audited.
- write-as learns move/copy (re-run `sudo agenthub server install-system`).
- Hardening: a write to a workspace re-checks its owner by the live role;
  S3 keeps its server.conf if the file stops reading and a tmux call never
  falls back to the host's server; the AI Proxy gateway marks what it relays
  and the dashboard refuses it; a NUL in a pane's argv/env is refused.
- OpenAPI for the new routes (server/openapi-extra/files.js); docs (API,
  SERVER-MODE, CLAUDE.md) updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 14:17:39 +07:00
parent b441f32949
commit e94038481e
38 changed files with 5961 additions and 27 deletions
+55
View File
@@ -258,6 +258,61 @@ folder name inside an existing folder, `400 EBADNAME`/`ENOTFOUND`,
not at all without one (`403 EFOLDER`); a member's folder is made as the
member (S3).
#### File Explorer
```
GET /api/lanes/:id/fs/list?path= { path, parent, entries: [{ name, path, type: dir|file|link|linkdir|other, size, mtime }], truncated }
POST /api/lanes/:id/fs/mkdir { path } → 201 { path }
POST /api/lanes/:id/fs/newfile { path } → 201 { path } (empty file)
POST /api/lanes/:id/fs/rename { path, name } → { path }
POST /api/lanes/:id/fs/move { paths, dest, conflict? } → { moved }
POST /api/lanes/:id/fs/copy { paths, dest, conflict?, fromLane? } → { copied }
POST /api/lanes/:id/fs/delete { paths } → { trashed: [ids] }
GET /api/lanes/:id/fs/trash → { items: [{ id, name, laneId, path, isDir, deletedAt }], keepDays }
POST /api/lanes/:id/fs/trash/restore { id } → { path, laneId }
POST /api/lanes/:id/fs/trash/purge { id | "all" } → { purged }
POST /api/lanes/:id/fs/upload?path=&conflict= raw body, streamed → 201 { path } | 200 { skipped: true }
GET /api/lanes/:id/fs/zip?p=<path>&p=… application/zip, streamed
```
The workspace **File Explorer** — the **Files** page in the sidebar (`/files?lane=<id>&path=<folder>`), laid out like GNOME Files (workspaces as places, Back/Forward, a path bar, icon grid or list, hidden files behind Ctrl+H), and the same manager in a dialog from a workspace's Files tab (the folder button; "Open in File Explorer" on a folder). `?session=` / `?source=` pick the same folder the
git reads do. Paths go through the read path's guards (`lanePath`, the parent
`resolveInLane`-confined, the last segment kept as a name so a symlink is acted
on as a link); nothing in `.git`, no secret file, never the workspace folder
itself (`400 EBADPATH` / `403 ESECRET`). Writes are same-origin and run as the
folder's account (a member's through `write-as`). `conflict` = `fail`
(default: `409 EEXISTS` with `error.names`), `skip`, `keep` (a free
"name (1).ext") or `replace` (the old one goes to the trash first). Delete
moves into the account's trash — a member's `~/.local/share/agenthub/trash/`,
the host's `<data dir>/trash/<user id>/` — restorable for 7 days. A copy may
come `fromLane` another workspace the caller can see, of the same account
(`403 EFOREIGN` otherwise). An upload is at most 500 MB (`413 ETOOBIG`, from
`Content-Length` before a byte is stored), a zip at most 2 GB / 50,000 files.
Under S3 a move/copy/delete needs the installed `write-as` to know
`move`/`copy`: `409 EUPGRADE` until `sudo agenthub server install-system` is
re-run.
#### File Explorer on a home
```
GET /api/homes { homes: [{ id: "me" | <username>, label, readOnly }] }
GET /api/homes/:who/fs/list|trash|zip|download as the workspace routes above
POST /api/homes/:who/fs/mkdir|newfile|rename|move|copy|delete|upload|trash/restore|trash/purge
```
The same File Explorer on a user's HOME (`server/lib/homes.js`): under S3 their
own Linux home, in server mode without S3 their folder under
`AGENTHUB_USERS_ROOT`, in local mode (or for an admin with no Linux account)
the host account's home. Every dot entry at the TOP of a home (`.ssh`,
`.claude`, `.config`, `.agenthub`, `.bashrc`…) is hidden from listings and
refused for every read and write — also through a symlink, a download or a
zip (`403 ESECRET`); dot files deeper down are ordinary. `:who` is `me`; an
admin may also name another user — read-only (`403 EREADONLY` on every
write), each read written to the audit log. A copy goes home ↔ workspace
(`fromHome` on the workspace route, `fromLane` on the home one) when both are
one Linux account; each side is checked under its own rule. A home is never a
workspace. The **Files** page opens on `?home=me` by default.
#### Clone a repository into a new workspace
```
+16
View File
@@ -713,6 +713,20 @@ Every command below runs as the **host account** (the one that runs the
dashboard) unless it says `sudo`. `agenthub server install-system` calls sudo
itself, once.
### After an update that changes the member helpers
`write-as`, `pane-launch` and the sudoers file are copied to
`/opt/agenthub/libexec` by `install-system` only — an update of AgentHub does
not touch root-owned files. When a release changes them (v2.7.7: `write-as`
learned `move`/`copy` for the File Explorer), re-run it once:
```bash
sudo agenthub server install-system
```
Until then the File Explorer's move, copy and delete in a member's folder
answer `409 EUPGRADE` (listing, upload, new folder/file and zip work).
### What a new machine needs before `install-system`
`install-system` does not do any of this:
@@ -955,6 +969,8 @@ To remove everything:
| `409 ENOTSHARED` on a member's start | That tool is not published to `/opt/agenthub/tools` yet: `agenthub server tools`. |
| `403 EMEMBERLANE` | Worktree mode and the runtime layer are not offered in a member's folder; use Repo mode and the prompt box's New worktree. |
| `400 EMEMBERCRED` | Older builds only: a member's typed git login was refused. Now it goes into their own store (Settings → Git → **Your git identity**). |
| `409 EOWNERFOLDER` (on a write to an existing workspace) | Its owner may no longer own that folder by their CURRENT role — typically an admin demoted to developer whose workspace is outside the users root (it runs as the host). Reads still work; an admin reassigns the workspace or restores the role. |
| `403 EGATEWAY` | A request relayed by the AI Proxy gateway reached the dashboard itself (a proxy profile whose base URL points back at AgentHub). Point the profile at the real proxy. |
| `409 ESTOREUNREADABLE` | The dashboard cannot read the member's `~/.config/agenthub/git-credentials` (the unit lost `CAP_DAC_READ_SEARCH`, or the file is not the member's own regular file); nothing is rewritten. Check `grep CapAmb /proc/<pid>/status` shows `0000000000000004`. |
| `403 EPEER` on a hook | The hook came from an account that is neither the host nor an enabled member, or the uid could not be read. |
| `403 EFOREIGN` on a hook | A member's hook named a folder, session or pane outside their own home. |