chore: v2.8.5 — Trash opens on one folder per user, server setup installs S3

- Files → Trash: ONE sidebar entry. An admin's opens on a folder per user's
  Trash (My Trash first, item counts from GET /api/homes/trash, an unreadable
  one showing its error); a folder opens that user's Trash (path bar
  Trash › user, Up back to the folders). An admin may restore/purge in
  another user's Trash (audited); purging asks first. The merged "All users"
  list is gone from the UI.
- A home's Trash is its user's whole recycle bin (home store + host store,
  items tagged `store`).
- `agenthub server setup` installs one Linux user per account on Linux by
  default (`--no-system` opts out); install-system messages drop `sudo`.
- Settings → Git: the author email suggests <username>@<AGENTHUB_EMAIL_DOMAIN>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 16:02:11 +07:00
parent 09efb089cf
commit 758609775f
27 changed files with 839 additions and 107 deletions
+29 -4
View File
@@ -292,7 +292,7 @@ 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
`move`/`copy`: `409 EUPGRADE` until `agenthub server install-system` is
re-run.
#### File Explorer on a home
@@ -311,7 +311,32 @@ the host account's home. Every dot entry at the TOP of a home (`.ssh`,
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
write), each read written to the audit log. The one exception is that user's
**Trash**.
A home's Trash is its USER's one recycle bin, whatever place they deleted in:
`GET …/trash` merges their home account's trash (a member's
`~/.local/share/agenthub/trash/`) with their host trash
(`<data dir>/trash/<user id>/`, what they deleted in a host-folder workspace)
and tags each item `store: "home" | "host"`; `POST …/trash/restore` and
`POST …/trash/purge` take `{id, store}` (`store` defaults to `home`;
`{id: "all"}` empties every store). A host item whose workspace is gone
answers `404 ENOFILE` (nowhere of that account to put it back). An admin may
restore and purge in ANOTHER user's Trash too, each written to the audit log
with the item id — so a member's deleted files can be restored or freed
without waiting the 7 days. `GET /api/homes/trash` merges every Trash the
caller may open into one list (`{items, errors, keepDays}`; an admin's is
every user's, each other user's read audited): each item carries `home` (the
`:who` its restore/purge goes to) and `owner`, and a user whose trash cannot
be read is listed in `errors` rather than left out. The Files sidebar has ONE
**Trash** entry: a user's opens their own Trash; an admin's opens on one
folder per user (**My Trash** first, each counted from that merged list, an
unreadable one showing its error), and opening a folder shows that user's
Trash (path bar **Trash › user**, Up goes back to the folders). A trash that
could not be read shows as an error rather than as empty, and deleting
forever asks first.
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.
@@ -1042,7 +1067,7 @@ cannot collide on `index.lock`.
#### Your git identity
```
GET /api/me/git { account: "member"|"host", writable, ssh: { type, publicKey, fingerprint, terminal: "on"|"off"|"foreign" } | null (the first key), sshKeys: [{ type, publicKey, fingerprint }] (ed25519 first), sshTerminal, https: [{ protocol, host, username, hasToken }] }
GET /api/me/git { account: "member"|"host", writable, ssh: { type, publicKey, fingerprint, terminal: "on"|"off"|"foreign" } | null (the first key), sshKeys: [{ type, publicKey, fingerprint }] (ed25519 first), sshTerminal, https: [{ protocol, host, username, hasToken }], author: { name, email }, defaultEmail }
POST /api/me/git/ssh-key Make the SSH key of `type` — Body: { replace?, type?: "ed25519" | "rsa" } (409 EEXISTS when that TYPE exists, without replace; the other type is kept — an account may hold one of each, and every ssh git offers both) → 201, same shape
DELETE /api/me/git/ssh-key Delete the key of `?type=ed25519|rsa`, or every key without it (404 ENOTFOUND, 400 EBADKEYTYPE) — also removes our terminal `core.sshCommand`
PUT /api/me/git/ssh-terminal Use the key for git typed in the account's own terminal — Body: { on } — sets/unsets `core.sshCommand` in its `~/.gitconfig` (ours first, then `~/.ssh/id_*`); on by default when a member makes a key; 409 EFOREIGNSSH over one the user wrote, which is never touched
@@ -1050,7 +1075,7 @@ PUT /api/me/git/ssh-user The SSH login name for one host — Body: { h
DELETE /api/me/git/ssh-user/:host Drop that rule (404 ENOTFOUND); any other `insteadOf` the user wrote is never touched
POST /api/me/git/https Save an https login — Body: { host, username, password }
DELETE /api/me/git/https/:host Delete one (404 ENOTFOUND)
PUT /api/me/git/author Commit author — Body: { name, email } (400 EBADAUTHOR) — `git config --global user.name/email` as the account
PUT /api/me/git/author Commit author — Body: { name, email } (400 EBADAUTHOR) — `git config --global user.name/email` as the account. A new SSH key is labelled with that email, else `defaultEmail` (`<username>@smartgift.vn`; `AGENTHUB_EMAIL_DOMAIN` overrides the domain), also returned by GET /api/me/git as the email form's suggestion
```
`GET` also answers `author: { name, email }` (null when unset). A member's