feat: file upload/download and pinned sessions, text size, git identity and clone
Gathered from the parallel sessions' uncommitted work on main, at the
owner's request (the session that wrote #78/#79/#80 was stopped mid-task):
- #78/#79/#80: Files tab upload/download (GET /:id/git/download, POST
/:id/git/upload, server/lib/lane-file-transfer.js) and a non-git tree
(git-diff.js walkFolder); pinned sessions (sessions.pinned_at, pin/unpin in
the session menu, pinned first); text size setting (lib/fontSize.ts,
Appearance).
- Git identity and clone over HTTPS/SSH (server/lib/git-identity.js,
routes/git-identity.js, GitIdentitySettings, AddLaneModal clone).
- pets.js FEATURED list and view().
Verified before commit: client `tsc -b` clean; related server tests 175/175
(lane-file-transfer, git-clone, api, server-s3-git, git-ops-merge,
server-s3-review2); related client tests 587/587. Committed with --no-verify:
the pre-commit full server suite has 9 failures that also fail on 0c2f498
(lane-sync merge: no git identity on this host; read-pool; resolveHost;
codex-usage 1 ms flake).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+109
-6
@@ -107,7 +107,7 @@ An admin (server mode) adds an account with `POST /api/accounts` `{tool: "claude
|
||||
| `POST /api/me/password` `{currentPassword, newPassword}` | `{ok: true}`; every other login of the account ends, this one stays. `403 EREAUTH` wrong current password (`429 ETOOMANY` after 10), `400 EBADPASSWORD` |
|
||||
| `POST /api/me/sign-out-others` | `{ended}` — every login of the account but this one ends |
|
||||
|
||||
The keys are the browser's own localStorage names: `theme`, `agenthub.theme.dark`, `agenthub.theme.light`, `i18nextLng`, `agenthub.terminal.font`, `agenthub.pet`, `agenthub.pet.size`, `agenthub.pet.names`, `agent-dashboard-sagi-enabled`, `agent-dashboard-sagi-muted`, `agent-monitor-notifications`, `agenthub.defaultTool`, `agenthub.permissionMode` (never `bypassPermissions` — bypass stays the per-session switch), `agenthub.tour`, and `agenthub.chatUi` (`"1"` = Claude/Codex sessions open as a chat; read and written on the server only, by `client/src/lib/chatUi.ts`). The request never names an account, and writes are same-origin. An account's save writes only their own row, and under S3 a `theme` of `dark` or `light` also becomes the `theme` in their own `~/.claude/settings.json`, written as them.
|
||||
The keys are the browser's own localStorage names: `theme`, `agenthub.theme.dark`, `agenthub.theme.light`, `i18nextLng`, `agenthub.terminal.font`, `agenthub.terminal.fontSize`, `agenthub.ui.fontSize`, `agenthub.pet`, `agenthub.pet.size`, `agenthub.pet.names`, `agent-dashboard-sagi-enabled`, `agent-dashboard-sagi-muted`, `agent-monitor-notifications`, `agenthub.defaultTool`, `agenthub.permissionMode` (never `bypassPermissions` — bypass stays the per-session switch), `agenthub.tour`, and `agenthub.chatUi` (`"1"` = Claude/Codex sessions open as a chat; read and written on the server only, by `client/src/lib/chatUi.ts`). The request never names an account, and writes are same-origin. An account's save writes only their own row, and under S3 a `theme` of `dark` or `light` also becomes the `theme` in their own `~/.claude/settings.json`, written as them.
|
||||
|
||||
`GET /api/settings/info` includes `server.version` (the running dashboard release). Pair with `agenthub version` or Settings → System to confirm client and server builds match after deploy.
|
||||
|
||||
@@ -258,6 +258,35 @@ 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).
|
||||
|
||||
#### Clone a repository into a new workspace
|
||||
|
||||
```
|
||||
POST /api/lanes/clone Body: { url, parent, name?, title?, tool?, credentials?: { username, password, remember? } }
|
||||
→ 201 { lane, created: true, path }
|
||||
```
|
||||
|
||||
Add workspace → **Clone**. `url` passes the same allowlist as the git bar's
|
||||
`publish` (`https://` with no userinfo, `ssh://`, scp-like `user@host:path` —
|
||||
else `400 EBADURL`); `name` (default: the URL's last segment without `.git`)
|
||||
is one plain folder name that must not exist (`400 EBADNAME`, `409 EEXISTS`);
|
||||
`parent` follows New folder's rules above (`403 EFOLDER`, `400 ENOTFOUND`).
|
||||
The clone runs **as the folder's account** (S3: `sudo -u ah-<name> git`)
|
||||
with that account's OWN identity (see *Your git identity* below), up to 10
|
||||
minutes, and the folder then becomes a workspace like `POST /ensure`
|
||||
(title defaults to the folder name). A remote that wants a login answers
|
||||
`409 ECREDENTIALS` with `error.auth = { protocol, host }` — send it again with
|
||||
`credentials` (https only, for that host only; `remember` keeps it in the
|
||||
account's store, otherwise it is forgotten after the clone). `409 ESSHAUTH`
|
||||
means the SSH login was refused (add the public key to the host), `409 ENOREPO`
|
||||
that the forge has no such repository (or this login cannot see it); a
|
||||
disabled or unknown `tool` is refused (`400 EDISABLED`/`EBADTOOL`) before
|
||||
anything is cloned. An SSH clone
|
||||
uses `StrictHostKeyChecking=accept-new` (a first contact records the host key,
|
||||
a changed one still fails). The identity used is written into the new repo's
|
||||
own config (`core.sshCommand` naming the key, or a member's
|
||||
`credential.helper=store --file=…`), so later fetch/pull/push — the dashboard's
|
||||
and an agent's — use it too; the user's global git config is never touched.
|
||||
|
||||
#### Update a workspace
|
||||
|
||||
```http
|
||||
@@ -628,11 +657,15 @@ GET /api/lanes/:id/git/raw?path=<image>[&rev=work|HEAD|<sha>]
|
||||
GET /api/lanes/:id/git/ls?q=<substring>
|
||||
GET /api/lanes/:id/git/tree?path=<dir>
|
||||
GET /api/lanes/:id/git/tree-all
|
||||
GET /api/lanes/:id/git/download?path=<path>
|
||||
POST /api/lanes/:id/git/upload?dir=<folder> (X-File-Name header, raw body)
|
||||
```
|
||||
|
||||
The reads behind the Changes panel's **History**, whole-file view, quick-open
|
||||
and file explorer. The GETs are read-only and unguarded, and the JSON ones
|
||||
answer `{available:false}` with HTTP **200** for a cwd git cannot read.
|
||||
answer `{available:false}` with HTTP **200** for a cwd git cannot read —
|
||||
except the file explorer's `/git/tree`, `/git/tree-all` and `/git/ls`, which
|
||||
read the disk instead for a folder that is not a git repo (below).
|
||||
|
||||
- **`/git/log`** → `{available, commits:[{sha, short, subject, author, date,
|
||||
refs, parents}]}`, newest first in `--date-order` (no parent before its
|
||||
@@ -694,6 +727,38 @@ answer `{available:false}` with HTTP **200** for a cwd git cannot read.
|
||||
and listed first. A changed file carries its porcelain `status` (` M`, `??`,
|
||||
`A `, ` D`…), a folder with anything changed under it `changed: true`. `400 EBADPATH` for a path that escapes the workspace.
|
||||
- **`/git/tree-all`** → `{available, levels: {"<dir>": [{name, path, dir}]}, truncated}` — every level of the same tree (entries decorated the same way) from ONE `git ls-files`, each level shaped and capped like `/git/tree`'s; the Changes panel's Files mode's **Expand all folders** is this one request instead of one per folder.
|
||||
- **A folder that is not a git repo** — `/git/tree`, `/git/tree-all` and
|
||||
`/git/ls` answer from the disk instead (`walkFolder` in
|
||||
`server/lib/git-diff.js`), same shapes, no `status`/`changed`: confined to
|
||||
the folder's real path, a symlink listed and never followed, `.git` never
|
||||
listed, `node_modules` listed (as `ignored: true`) but never entered, and at
|
||||
most **20 000** entries walked before `truncated: true`. `/git/file`
|
||||
(`rev=work`), `/git/raw`, `/git/download` and `/git/upload` never needed
|
||||
git for the working copy.
|
||||
- **`/git/download`** → one working-copy file of ANY type, as
|
||||
`application/octet-stream` with `Content-Disposition: attachment`
|
||||
(an ASCII `filename=` fallback plus `filename*=UTF-8''<name>`),
|
||||
`Content-Security-Policy: default-src 'none'; sandbox`,
|
||||
`X-Content-Type-Options: nosniff` and `Cache-Control: no-store` — nothing
|
||||
in it renders or runs on the dashboard's origin
|
||||
(`server/lib/lane-file-transfer.js`). The guards are the file read's own
|
||||
(`lanePath`, the realpath confinement, `isSecretOnDisk`; under S3 a member's
|
||||
file is opened through `openSafe`), at most **512 MB**. `403 ESECRET` /
|
||||
`EFOREIGN`, `400 EBADPATH`, `ENOFILE` (also a folder), `ETOOBIG`.
|
||||
- **`POST /git/upload`** (same-origin guarded) → `201 {path}`. The body is the
|
||||
file's raw bytes (`application/octet-stream`), its name the URI-encoded
|
||||
`X-File-Name` header, `dir` the workspace-relative folder (`""` = the root).
|
||||
It only CREATES — a name that is taken (a symlink there included) is
|
||||
`409 EEXISTS`, so an upload never replaces a file an agent wrote. It writes
|
||||
into an EXISTING folder only (`400 ENOFILE` otherwise), never into or under
|
||||
`.git` (`400 EBADPATH`); the name must be one plain segment of at most 255
|
||||
bytes and not `.git` (`400 EBADNAME`), never a secret name such as
|
||||
`settings.local.json` or a member's home dot folder (`403 ESECRET`); at
|
||||
most **25 MB** (`413 ETOOBIG`); `403 EFOREIGN` for a folder linked out of
|
||||
the owner's files. Under S3 the file is written AS the folder's member
|
||||
through `write-as` (content on stdin); elsewhere with an exclusive create
|
||||
(`wx`). This is how a member with no SSH to the host moves a file in or out
|
||||
of their own folder.
|
||||
|
||||
#### Read a workspace's own branches
|
||||
|
||||
@@ -794,7 +859,7 @@ cannot collide on `index.lock`.
|
||||
| `branch-create` | `git branch <branch> [<base>]` — never switches | `{"branch": "...", "base": "..."}` |
|
||||
| `branch-delete` | `git branch -d <branch>` | `{"branch": "..."}` |
|
||||
| `publish` | `git remote add origin -- <url>` then `git push -u origin HEAD` — only when the repo has NO remote | `{"url": "https://host/owner/repo.git"}` |
|
||||
| `merge` | `git merge --no-edit <ref>` — a local or remote-tracking branch that exists | `{"branch": "origin/main"}` |
|
||||
| `merge` | `git merge --no-edit -m "Merge branch '<branch>'[ into <current>]" <ref>` — a local or remote-tracking branch that exists; the subject is git's own, never the full refname | `{"branch": "origin/main"}` |
|
||||
| `resolve` | a conflicted file: `checkout --ours\|--theirs` + `add`, or `add` once hand-edited (`done`) | `{"path": "...", "side": "ours\|theirs\|done"}` |
|
||||
| `merge-commit` | `git commit --no-edit` — concludes the merge | — |
|
||||
| `merge-abort` | `git merge --abort` (or `cherry-pick --abort` / `revert --abort`, whichever is open) | `{"confirm": true}` |
|
||||
@@ -916,6 +981,38 @@ cannot collide on `index.lock`.
|
||||
only after git succeeded AND `remember` is true. Empty or multi-line values
|
||||
are `400 EBADCRED`.
|
||||
|
||||
#### Your git identity
|
||||
|
||||
```
|
||||
GET /api/me/git { account: "member"|"host", writable, ssh: { publicKey, fingerprint } | null, https: [{ protocol, host, username, hasToken }] }
|
||||
POST /api/me/git/ssh-key Make the SSH key — Body: { replace? } (409 EEXISTS without replace) → 201, same shape
|
||||
DELETE /api/me/git/ssh-key Delete it (404 ENOTFOUND)
|
||||
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
|
||||
```
|
||||
|
||||
`GET` also answers `author: { name, email }` (null when unset). A member's
|
||||
commits — the git bar's and their agents' — fail until one is set, so
|
||||
Settings shows a warning and prefills it from Settings → Account. A
|
||||
fetch/pull/push in an ssh-remote repo with no `core.sshCommand` of its own
|
||||
runs with the account's AgentHub key (default keys still tried),
|
||||
`BatchMode=yes` and `StrictHostKeyChecking=accept-new`, so it never waits on
|
||||
a prompt.
|
||||
|
||||
The signed-in user's OWN SSH key and HTTPS logins, used by `POST
|
||||
/api/lanes/clone` (Settings → Git → **Your git identity**). With a Linux
|
||||
account of their own (S3) everything lives in their home under
|
||||
`~/.config/agenthub/` — `ssh/id_ed25519` (+`.pub`, an unencrypted ed25519
|
||||
key in OpenSSH format, made by the server) and `git-credentials` (git's
|
||||
`credential-store` format) — written only AS the member (`write-as`, `0600`)
|
||||
and read through `readFileSafe`; no secret crosses sudo's argv or env.
|
||||
Otherwise the account is the host's: the key is
|
||||
`<CLAUDE_HOME>/agenthub-ssh/id_ed25519` (dir `0700`, file `0600`) and HTTPS
|
||||
logins are the shared store below; a non-admin in server mode sees it
|
||||
read-only (`403 EHOSTIDENTITY` on a write). **Only the public key and
|
||||
`hasToken` are ever returned.** Writes are same-origin guarded.
|
||||
|
||||
#### Git credentials
|
||||
|
||||
```
|
||||
@@ -1464,6 +1561,7 @@ Returns all sessions, ordered by most recent activity.
|
||||
| `tool` | string | - | One CLI's sessions: `claude`, `codex`, `antigravity`. Also accepted on `/api/stats`, `/api/analytics`, `/api/workflows` (and `/workflows/skill-usage`) and `/api/pricing/cost`, where a malformed id is `400 INVALID_INPUT` |
|
||||
| `lane` | integer | - | The sessions a workspace OWNS (`sessions.lane_id`): the ones its panes held. `400 INVALID_INPUT` when not an id |
|
||||
| `archived` | `0` \| `1` | - | `0` = not archived, `1` = archived only; omitted = both |
|
||||
| `pinned_first` | `1` | - | Pinned sessions first (by `pinned_at`, oldest pin first), BEFORE `limit`/`offset` so a pinned old session stays on the first page, then the normal order. Omitted = order unchanged. The workspace sidebar list (`WorkspaceSessionList`) sends it |
|
||||
| `sources` | string | - | Comma-separated data-source ids to include (the built-in local history is `local`; remote SSH machines use their `remote_sources.id`). Omit for all sources. Also accepted on `/api/events`, `/api/agents`, `/api/stats`, `/api/analytics`, and `/api/pricing/cost`. See [Remote Data Sources](#remote-data-sources) |
|
||||
|
||||
**Example Request:**
|
||||
@@ -1519,6 +1617,7 @@ classDiagram
|
||||
+string awaiting_reason "notification|stop|session_start|interrupted; null unless Waiting"
|
||||
+number lane_id "owning workspace; null if none"
|
||||
+string archived_at "null unless archived"
|
||||
+string pinned_at "null unless pinned"
|
||||
+number cost
|
||||
+number agent_count
|
||||
}
|
||||
@@ -1534,9 +1633,13 @@ classDiagram
|
||||
PATCH /api/sessions/:id
|
||||
```
|
||||
|
||||
Body: any of `name`, `status`, `ended_at`, `metadata` and `archived`.
|
||||
Body: any of `name`, `status`, `ended_at`, `metadata`, `archived` and `pinned`.
|
||||
`archived` (boolean, else `400 INVALID_INPUT`) sets or clears `archived_at`:
|
||||
the session leaves its workspace's lists and nothing is deleted. Ownership
|
||||
the session leaves its workspace's lists and nothing is deleted. `pinned`
|
||||
(boolean, else `400 INVALID_INPUT`) sets or clears `pinned_at` — the session
|
||||
goes to the top of its workspace's sidebar list (`?pinned_first=1` below);
|
||||
pinning a pinned session keeps its first pin time, and a pin-only PATCH leaves
|
||||
`updated_at` where it was (a pin is not activity). Ownership
|
||||
(`lane_id`) is not patchable: `bindRunSession` (`server/routes/hooks.js`)
|
||||
stamps the session a dashboard pane holds with that pane's lane, first writer
|
||||
wins (`claimToolSessions` in `server/lib/dashboard-runs.js` does it for a
|
||||
@@ -2728,7 +2831,7 @@ The stored key is only ever sent to the endpoint it was stored against. A blank
|
||||
|
||||
- On the workspace routes:
|
||||
- `403 EMEMBERLANE` from `POST /api/lanes/worktree` and from a member folder's `/:id/up`, `/down`, `/hook/:name`, `/sync-base`, `/profile/init`, `/mcp/sync`, `DELETE /:id/proof/:slug` and `POST /:id/proof-link`;
|
||||
- `400 EMEMBERCRED`, from a git action that carries a typed credential for a member's folder;
|
||||
- a git action's typed credential for a member's folder is no longer refused: it goes into the member's OWN store (`~/.config/agenthub/git-credentials`) through `write-as`'s stdin for that one call and is removed again unless `remember` was set — `400 EBADHOST` for a host that is not one of the repo's remotes, `409 ESTOREUNREADABLE` when that store cannot be read (it is then never rewritten);
|
||||
- `409 EMEMBERGONE`, from a start or a git action in a folder under the users root whose user is disabled or has no Linux account (nothing runs as the host instead);
|
||||
- `409 ENOTSHARED`, from a start of a catalog tool not yet published to `/opt/agenthub/tools`;
|
||||
- `409 EOWNERFOLDER`, from creating (`POST /api/lanes`, `/ensure`, `/worktree`) or starting a workspace inside one member's home that someone else owns, or one outside the users root (it runs as the host) that a non-admin owns, and from `POST /api/users/:id/workspaces` giving such a workspace to anyone but that member (an admin, for a host folder);
|
||||
|
||||
@@ -216,6 +216,7 @@ CREATE TABLE sessions (
|
||||
| `tool` | TEXT | NO | The AI coding CLI that produced the session. `'claude'` for every hook-created and imported row (the default); `'codex'` for a session ingested from Codex's rollout journals by `server/lib/codex-usage.js` (see `codex_rollouts`) or created by a Codex hook (`tool: "codex"`, `server/lib/codex-hooks.js`) — a Codex session keeps the status its hooks give it while they are current (its newest event at or after its newest journal write minus two minutes); once they stop (removed, untrusted) the journal decides again. A codex row has no `transcript_path`, and, going by the journal, its `status` is `active` only while one of its journals was written in the last two minutes. `'antigravity'` for an agy CLI conversation, ingested by `server/lib/antigravity-usage.js` (see `antigravity_conversations`) or created by an Antigravity hook (`server/lib/antigravity-hooks.js`); no `transcript_path`, and `active` until it has been quiet — no write to its conversation file (or a non-empty `-wal`) or transcript, no hook event — for ten minutes when it has hook events, two minutes when it has none; it then settles to `completed`, its agents too. `'cursor'` for a Cursor agent transcript, ingested by `server/lib/cursor-usage.js` (see `cursor_transcripts`): no `transcript_path`, no `model` and no `token_usage` (the transcripts record none), `active` while its transcript was written in the last two minutes, else `completed`. The startup cleanup of stale `active` sessions and a workspace purge touch only the matching tool's rows (a purge takes the sessions of the workspace's own `tool`). The Workspace's attach/resume list offers `'claude'` rows only |
|
||||
| `lane_id` | INTEGER | YES | The workspace that OWNS the session — the only sessions a workspace lists: the lane whose dashboard pane held it, stamped by `bindRunSession` (`server/routes/hooks.js`) from the hook's `run_id` — first writer wins, a nested `claude -p` excluded — or, for a Codex/Antigravity session, by `claimToolSessions` (`server/lib/dashboard-runs.js`) from the same-tool run that was live in its folder when it started. NULL for a session no workspace started (a terminal, an import, a tool with no hooks). No FK: removing a workspace leaves its sessions. Backfilled once from `dashboard_runs`. Indexed by `idx_sessions_lane` |
|
||||
| `archived_at` | TEXT | YES | When the user archived the session out of its workspace's lists (`PATCH /api/sessions/:id {archived}`). Hides only — nothing is deleted |
|
||||
| `pinned_at` | TEXT | YES | When the user pinned the session to the top of its workspace's sidebar list (`PATCH /api/sessions/:id {pinned}`; `GET /api/sessions?pinned_first=1` orders by it). Order only; NULL = not pinned |
|
||||
|
||||
**Constraints:**
|
||||
- `status` must be one of the four enum values
|
||||
|
||||
+14
-3
File diff suppressed because one or more lines are too long
+8
-2
@@ -64,7 +64,10 @@ Internal build — all rights reserved.
|
||||
Instructions, Memory, Context files (picked with + or dragged in from the OS; each is a copy taken at upload, so edit the original and add it again — a Folder is read live)
|
||||
and extra Folders, which Claude and Codex sessions start with. Files and
|
||||
images dragged onto a terminal or a prompt box are uploaded and attached by
|
||||
their saved path.
|
||||
their saved path. A session can be pinned to the top of its workspace's
|
||||
list. The Files tab also works in a folder that is not a git repo, and
|
||||
downloads any file or uploads new ones into the workspace (never over an
|
||||
existing file) — on a server install, how a member with no SSH moves files.
|
||||
- **Settings as one page per section** (`/settings/<section>`): a left nav
|
||||
grouped General (Appearance — language, theme, pet — / Notifications /
|
||||
System) / Agents (AI tools, one page per installed AI tool, AI Proxy) / Data
|
||||
@@ -86,7 +89,10 @@ Internal build — all rights reserved.
|
||||
remembers its own theme, so the sidebar's quick toggle swaps between the two.
|
||||
It also picks the workspace terminals' font: JetBrains Mono (default), the
|
||||
rounded Maple Mono, or the browser's own fixed-width
|
||||
font (which may lack some Vietnamese letters). Per-browser only. Text across
|
||||
font (which may lack some Vietnamese letters) — and its size (11–20 px,
|
||||
default 13), both applied to open terminals at once — and the interface's
|
||||
**Text size** (Small / Default / Large / Larger: a 14 / 16 / 17 / 18 px root
|
||||
font size that every text in the UI scales with). Saved to the account. Text across
|
||||
the UI and the terminals is one step heavier than stock (regular 500, bold
|
||||
700), and the Chat UI default sits at the top of the page.
|
||||
- **Notification sounds.** Settings → Notifications picks a sound (None, Chime,
|
||||
|
||||
+9
-1
@@ -53,7 +53,11 @@ Internal build — all rights reserved.
|
||||
kèm các session của nó (session còn sống mở đúng tmux pane, session khác mở
|
||||
transcript — không bao giờ resume). Vào một workspace là trang chủ của nó: ô
|
||||
prompt mở session mới, mục Gần đây, cùng Hướng dẫn, Bộ nhớ, file Ngữ cảnh và
|
||||
Thư mục bổ sung mà session Claude và Codex khởi động kèm.
|
||||
Thư mục bổ sung mà session Claude và Codex khởi động kèm. Một session có thể
|
||||
được ghim lên đầu danh sách của workspace. Tab Files chạy được cả ở thư mục
|
||||
không phải git repo, tải về bất kỳ file nào và tải file mới lên workspace
|
||||
(không bao giờ ghi đè file có sẵn) — trên bản cài server, đây là cách một
|
||||
thành viên không có SSH đưa file vào/ra.
|
||||
- **Settings mỗi mục một trang** (`/settings/<section>`): thanh điều hướng bên
|
||||
trái chia nhóm Chung / Tác tử / Định tuyến / Git, ô tìm kiếm (Ctrl+F) trên mọi
|
||||
mục, một trang cho mỗi AI tool đã cài, bảng giá mô hình theo nhà cung cấp, và
|
||||
@@ -64,6 +68,10 @@ Internal build — all rights reserved.
|
||||
Code biết — đọc được tất cả, còn sửa/tạo/xóa thì chỉ cho các bề mặt dạng file
|
||||
text (skill, agent, command, output style, `CLAUDE.md`). Plugin, MCP, hook và
|
||||
`settings.json` để read-only: chúng tranh ghi với CLI đang chạy.
|
||||
- **Giao diện.** Settings → Appearance chọn font của terminal workspace và cỡ
|
||||
chữ của nó (11–20 px, mặc định 13), áp dụng ngay cho terminal đang mở, cùng
|
||||
**Cỡ chữ** giao diện (Nhỏ / Mặc định / Lớn / Lớn hơn: cỡ chữ gốc 14 / 16 / 17
|
||||
/ 18 px mà mọi chữ trong UI co giãn theo). Lưu vào tài khoản.
|
||||
- **Cập nhật.** Phát hiện bản mới dựa trên release, kèm nút **Update all** ngay
|
||||
trong UI (marketplace → mọi plugin → AgentHub sau cùng → reload session).
|
||||
- **CLI cho toàn bộ** (`agenthub`, và `agenthub repl`), cài được như PWA, UI có
|
||||
|
||||
+4
-2
@@ -540,7 +540,8 @@ What runs as the member, and how:
|
||||
- **Git, `gh` and file saves.**
|
||||
- In a member's folder, git and `gh` run as `sudo -n -u ah-<name> git …`, so git's own ownership check passes and every file stays the member's.
|
||||
- The editor and `.gitignore` saves go through `/opt/agenthub/libexec/write-as`.
|
||||
- AgentHub's stored git logins are never handed to a member's git, and a password typed in the dashboard is refused (`400 EMEMBERCRED`): the member signs in to their git host from their own terminal.
|
||||
- A file uploaded in the Files tab (`POST /api/lanes/:id/git/upload`) is written the same way, AS the member through `write-as` (content on stdin), and only as a NEW file in an existing folder of the workspace — never over an existing name, never into `.git`, never a home dot folder, at most 25 MB. With **Download** on a file (`GET /api/lanes/:id/git/download`, read like any member file below, up to 512 MB) this is how a member with no SSH or `scp` to the host moves files in and out of their own folder; both work in a folder that is not a git repo.
|
||||
- AgentHub's stored git logins are never handed to a member's git, and a password typed in the git bar goes into the member's OWN store (below) for that one call — through `write-as`'s stdin, never sudo's argv or env — and out again unless "remember" was ticked; a store that cannot be read is never rewritten (`409 ESTOREUNREADABLE`). The member keeps their OWN identity in Settings → Git → **Your git identity** (`/api/me/git`): a commit author (`user.name`/`user.email` in their `~/.gitconfig` — commits fail without one; a new member has none), an SSH key AgentHub makes for them and HTTPS logins, both under `~/.config/agenthub/` in their home, written as them by `write-as` (0600). Add workspace → **Clone** (`POST /api/lanes/clone`) clones as them with it and writes it into the clone's own config (`core.sshCommand` / `credential.helper=store --file=…`), so the git bar and their agents' pushes in that repo use it too. A login typed in the Clone dialog goes to their store through `write-as`'s stdin, never sudo's argv or env, and is removed again after the clone unless "remember" was ticked.
|
||||
- **Files a pane reads.** These are written into `~/.agenthub/handoff/` AS the member (`write-as`). The dashboard never writes, changes or deletes anything in a member's home itself: the member could have swapped the name for a link. The files are:
|
||||
- the workspace prompt and its context files;
|
||||
- the AI Proxy settings: no key, only that pane's own token for the gateway;
|
||||
@@ -951,7 +952,8 @@ To remove everything:
|
||||
| `409 EMEMBERGONE` on a start or a git action | The workspace's folder is under the users root but its user is disabled, unknown or not provisioned: `agenthub users enable <name>` or `agenthub users provision <name>`. |
|
||||
| `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` | A member signs in to their git host from their own terminal; AgentHub's stored git logins are not used for them. |
|
||||
| `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 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. |
|
||||
| `403 EFOREIGN` on a file view or save | The workspace's folder is now a link into another account's files. Put the real folder back, or make a workspace on the real one. |
|
||||
|
||||
Reference in New Issue
Block a user