agentus 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/README.zh-CN.md +396 -0
- package/docs/dev/01-dev-loop.md +186 -0
- package/docs/dev/02-verify.md +237 -0
- package/docs/dev/03-release.md +218 -0
- package/docs/dev/04-recover.md +199 -0
- package/docs/dev/05-agent-driven.md +114 -0
- package/docs/dev/README.md +65 -0
- package/docs/installation.md +366 -0
- package/docs/installation.zh-CN.md +350 -0
- package/package.json +5 -4
- package/packages/server/mock/agent.mjs +35 -0
- package/packages/server/src/fs.ts +216 -2
- package/packages/server/src/index.ts +87 -5
- package/packages/server/src/pty-helper.py +135 -0
- package/packages/server/src/term.ts +69 -28
- package/packages/web/dist/assets/index-D667PX2c.css +32 -0
- package/packages/web/dist/assets/index-d9NzwPFu.js +283 -0
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/index-B52l55_4.css +0 -1
- package/packages/web/dist/assets/index-BRgC7oYt.js +0 -246
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Agentus
|
|
2
2
|
|
|
3
|
+
[English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
3
5
|
**Keep your agents on the track.** A multi-session web cockpit for ACP-speaking coding
|
|
4
6
|
agents: open several sessions in the browser, each one backed by a real
|
|
5
7
|
`hermes acp` / `qodercli --acp` subprocess, and drive them from any device.
|
|
@@ -20,12 +22,22 @@ agents: open several sessions in the browser, each one backed by a real
|
|
|
20
22
|
## Install
|
|
21
23
|
|
|
22
24
|
```bash
|
|
23
|
-
# Node >= 22.5
|
|
25
|
+
# public one-liner — the published package (Node >= 22.5; the store is node:sqlite)
|
|
24
26
|
npx agentus # …or: npm i -g agentus, then `agentus`
|
|
25
27
|
# open http://localhost:8787 and log in (admin / 123456 — see "Login" below),
|
|
26
28
|
# then "+ new session", pick a backend + a working directory
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
Or run the installer straight from the repository — same install, plus a Node check and a
|
|
32
|
+
per-user fallback (`sudo` never):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -fsSL https://raw.githubusercontent.com/agentus-org/agentus/main/scripts/install.sh | bash
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Full guide — install, flags, every environment variable, login, TLS, the phone bridge:
|
|
39
|
+
[`docs/installation.md`](docs/installation.md) · [中文](docs/installation.zh-CN.md).
|
|
40
|
+
|
|
29
41
|
`npx agentus` runs the cockpit in the foreground — Ctrl-C stops it, and it takes its agent
|
|
30
42
|
children with it. Flags: `--port 9000`, `--data <dir>`, `--open` (open the browser once it
|
|
31
43
|
is up), `--where` (print the paths the server actually resolved — data dir, web bundle,
|
|
@@ -42,13 +54,18 @@ The first slot you start will use the Hermes home **your own agent already uses*
|
|
|
42
54
|
### From a checkout (development)
|
|
43
55
|
|
|
44
56
|
```bash
|
|
45
|
-
git clone https://github.com/
|
|
57
|
+
git clone https://github.com/agentus-org/agentus.git
|
|
46
58
|
cd agentus
|
|
47
59
|
npm start # installs if needed, builds, serves (scripts/start.sh)
|
|
48
60
|
npm run dev -w @agentus/web # front-end hot reload (Vite :5173, host: true → reachable over LAN)
|
|
49
61
|
npm run agentus # the same CLI as `npx agentus`, straight from the checkout
|
|
50
62
|
```
|
|
51
63
|
|
|
64
|
+
**Working on Agentus itself?** The bootstrapping manual is in
|
|
65
|
+
[`docs/dev/`](docs/dev/README.md): run a dev instance beside live, prove a change with the
|
|
66
|
+
browser sweeps, promote it to live, and what to do when live breaks. Written for a human
|
|
67
|
+
*and* for a coding agent you point at this repo.
|
|
68
|
+
|
|
52
69
|
`npm start` (→ `scripts/start.sh`) checks the Node version, installs dependencies with
|
|
53
70
|
`NODE_ENV=development` when `node_modules` is missing, builds when the web bundle is
|
|
54
71
|
stale, then serves. Use `AGENTUS_PORT=9000 npm start` to move the port.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
[English](README.md) | 简体中文
|
|
2
|
+
|
|
3
|
+
# Agentus
|
|
4
|
+
|
|
5
|
+
**让 agent 待在正轨上。** 一个面向会说 ACP 的编码 agent 的多会话 Web 驾驶舱:在浏览器里开多个会话,
|
|
6
|
+
每个会话背后是一个真实的 `hermes acp` / `qodercli --acp` 子进程,你可以从任何设备驱动它们。
|
|
7
|
+
|
|
8
|
+
> **红线:** UI 里**不包含任何智能**。没有 agent 循环,没有模型调用,没有 prompt 工程。服务端只做
|
|
9
|
+
> 六件事 —— 门禁、拉起子进程、转发 ACP JSON-RPC、把事件流推给浏览器、持久化 transcript、把权限请求
|
|
10
|
+
> 抛出来。推理全在 CLI 进程里。
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
[browser React SPA] ──HTTP + WS──▶ [Agentus server] ──ACP/JSON-RPC over stdio──▶ [hermes acp | qodercli --acp]
|
|
14
|
+
· session registry (who runs, where, pid)
|
|
15
|
+
· ACP client (ClientSideConnection)
|
|
16
|
+
· event fan-out → WS, commands → ACP
|
|
17
|
+
· SQLite transcript + resume
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 对外一条命令 —— 用 npm 上发布的包(Node >= 22.5,存储用 node:sqlite)
|
|
24
|
+
npx agentus # 或者 npm i -g agentus 然后直接跑 `agentus`
|
|
25
|
+
# 打开 http://localhost:8787,用 admin / 123456 登录(见下面「登录」),
|
|
26
|
+
# 然后「+ 新建会话」,选一个后端和一个工作目录
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
也可以直接从仓里跑安装脚本 —— 同一件事,多了 Node 版本检查和装到用户目录的回退(**绝不**用 `sudo`):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
curl -fsSL https://raw.githubusercontent.com/agentus-org/agentus/main/scripts/install.sh | bash
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
完整指南 —— 安装、参数、所有环境变量、登录、TLS、手机桥:
|
|
36
|
+
[`docs/installation.zh-CN.md`](docs/installation.zh-CN.md) · [English](docs/installation.md)。
|
|
37
|
+
|
|
38
|
+
`npx agentus` 在前台运行驾驶舱 —— Ctrl-C 停掉它,它会把 agent 子进程一起带走。参数:`--port 9000`、
|
|
39
|
+
`--data <dir>`、`--open`(起来后自动打开浏览器)、`--where`(打印服务端实际解析出的路径 —— 数据目录、
|
|
40
|
+
web bundle、配套 APK —— 然后直接退出,不启动任何东西)。
|
|
41
|
+
|
|
42
|
+
**npm 装不上的那个前置条件是一个 agent CLI**:`hermes acp`(把 Hermes 装到 PATH 上;`hermes acp --check`
|
|
43
|
+
可自检)或 `qodercli --acp`(先 `qodercli login`,否则 `newSession` 会以 `-32000` 失败)。没有它驾驶舱
|
|
44
|
+
照样能跑、照样能登录 —— 只是没有后端可以开会话。
|
|
45
|
+
|
|
46
|
+
你启动的第一个 slot 会使用**你自己的 agent 已经在用的**那个 Hermes home(`~/.hermes`),除非后端那一行
|
|
47
|
+
指定了另一个 —— 见下面的「HERMES_HOME」。
|
|
48
|
+
|
|
49
|
+
### 从 checkout 安装(开发)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/agentus-org/agentus.git
|
|
53
|
+
cd agentus
|
|
54
|
+
npm start # installs if needed, builds, serves (scripts/start.sh)
|
|
55
|
+
npm run dev -w @agentus/web # front-end hot reload (Vite :5173, host: true → reachable over LAN)
|
|
56
|
+
npm run agentus # the same CLI as `npx agentus`, straight from the checkout
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**要改 Agentus 本身?** 自举开发手册在 [`docs/dev/`](docs/dev/README.md):怎么在 live 旁边跑一个 dev 实例、
|
|
60
|
+
怎么用浏览器 sweep 证明改动有效、怎么发到 live、live 坏了怎么救。既是给人看的,也是给「丢给 coding agent
|
|
61
|
+
自己开发」用的。
|
|
62
|
+
|
|
63
|
+
`npm start`(→ `scripts/start.sh`)会检查 Node 版本,在 `node_modules` 缺失时用 `NODE_ENV=development`
|
|
64
|
+
安装依赖,在 web bundle 过期时构建,然后启动服务。用 `AGENTUS_PORT=9000 npm start` 换端口。
|
|
65
|
+
|
|
66
|
+
### 状态放在哪
|
|
67
|
+
|
|
68
|
+
默认 `~/.agentus` —— 里面有 `agentus.sqlite`(会话与 transcript)、`credentials.json`、`auth.token`
|
|
69
|
+
(机器凭证)、`auth.secret`、`tls/`、`settings.json`。首次启动时以 0700 创建,启动日志会打印它**以及
|
|
70
|
+
这个目录获胜的原因**:
|
|
71
|
+
|
|
72
|
+
| 顺序 | 规则 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 1 | `AGENTUS_DATA` 被设置时用它 —— 每个启动器和 dev/QA 实例都会钉住它 |
|
|
75
|
+
| 2 | `~/.agentus` 一旦里面有东西,或者仓库侧还没有任何数据时用它 |
|
|
76
|
+
| 3 | `<repo>/packages/server/.data` —— 早于这套布局的老 checkout |
|
|
77
|
+
|
|
78
|
+
规则 3 不是锦上添花:一个悄悄换了目录的安装会以空的会话列表启动,那读起来完全就是「我的会话没了」。
|
|
79
|
+
要迁移这样的 checkout,用 `AGENTUS_DATA=~/.agentus` 启动一次即可。`agentus --where` 会打印服务端解析
|
|
80
|
+
出的结果,而不用你自己猜。
|
|
81
|
+
|
|
82
|
+
### 后端
|
|
83
|
+
|
|
84
|
+
| id | 命令 | 说明 |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `hermes` | `hermes acp` | 需要 PATH 上有 Hermes 安装(`hermes acp --check`) |
|
|
87
|
+
| `qoder` | `qodercli --acp` | 需要先 `qodercli login`,否则 `newSession` 以 `-32000` 失败 |
|
|
88
|
+
| `mock` | `node packages/server/mock/agent.mjs` | 离线 dev/QA:假流式、权限请求、crash/plan/tool 触发 |
|
|
89
|
+
|
|
90
|
+
加一个后端就是 `packages/server/src/acp/backends.ts` 里的**一行**。协议层永远不动。
|
|
91
|
+
|
|
92
|
+
### 环境变量
|
|
93
|
+
|
|
94
|
+
| 变量 | 默认值 | 含义 |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `AGENTUS_PORT` | `8787` | 服务端口(从不读取裸 `PORT` —— 这个名字在共享主机上被污染) |
|
|
97
|
+
| `AGENTUS_DATA` | `~/.agentus` | 状态所在;完整顺序见「状态放在哪」(已有数据的老 checkout 上是 `<repo>/packages/server/.data`) |
|
|
98
|
+
| `AGENTUS_HERMES_CMD` | `hermes` | hermes 后端要拉起的二进制 |
|
|
99
|
+
| `AGENTUS_QODER_CMD` | `~/.local/bin/qodercli` | qoder 同上 |
|
|
100
|
+
| `AGENTUS_HERMES_HOME` | `~/.hermes` | 未指定 home 的那一行所用的默认 `HERMES_HOME`(运维者自己的 home) |
|
|
101
|
+
| `AGENTUS_PERM_TIMEOUT_MS` | `300000` (5 min) | 权限请求自动取消前等待多久 |
|
|
102
|
+
| `AGENTUS_HISTORY_PAGE` | `500` | transcript 分页大小(测试里也会调小以覆盖分页路径) |
|
|
103
|
+
| `AGENTUS_TERM_PTY` | 未设置 | `1` = 工作区终端走 Python 标准库 `pty`(真 tty;需要 `python3`)而不是管道 |
|
|
104
|
+
| `AGENTUS_TERM_CMD` | 未设置 | 完全覆盖终端命令行(例如 `socat …`),空格分隔 |
|
|
105
|
+
| `AGENTUS_TLS_PORT` | 存在证书时 `8443`,否则关闭 | **第二个监听器,TLS** —— 公网隧道指向的那个(`0` 关闭它) |
|
|
106
|
+
| `AGENTUS_TLS_CERT` / `AGENTUS_TLS_KEY` | `<AGENTUS_DATA>/tls/{cert,key}.pem` | 该监听器的证书材料(见 `scripts/make-cert.sh`) |
|
|
107
|
+
| `AGENTUS_TTS_BASE_URL` | 未设置 | **服务端**语音合成的 OpenAI 兼容 base(`…/v1`)。未设置 = 只用浏览器语音 |
|
|
108
|
+
| `AGENTUS_TTS_API_KEY` / `AGENTUS_TTS_MODEL` / `AGENTUS_TTS_VOICE` | – / `tts-1` / `alloy` | 同上 |
|
|
109
|
+
| `AGENTUS_STT_BASE_URL` | 未设置 | **服务端**转写的 OpenAI 兼容 base。未设置 = 只用浏览器识别 |
|
|
110
|
+
| `AGENTUS_STT_API_KEY` / `AGENTUS_STT_MODEL` / `AGENTUS_STT_LANGUAGE` | – / `whisper-1` / – | 同上 |
|
|
111
|
+
| `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` | – | 百炼 (DashScope) 凭证。既从进程 env 读,**也**从 `~/.hermes/.env` 读,因为服务端通常是从一个普通 shell 启动的 |
|
|
112
|
+
|
|
113
|
+
语音/主题变量只是**引导值**:设置页(侧栏里的 ⚙)才是 provider、endpoint、key、模型、热词表和配色的
|
|
114
|
+
所有者,它把这些存进 `<AGENTUS_DATA>/settings.json`(0600,key 从不回传给浏览器)。以页面里说的为准;
|
|
115
|
+
env 的存在只是让一个全新的 checkout 在没人打开设置页之前就能用。
|
|
116
|
+
|
|
117
|
+
## 登录
|
|
118
|
+
|
|
119
|
+
驾驶舱默认在登录后面 —— 这东西会拉起往你磁盘上写东西的进程,所以一个开放的端口就是一个随时会发生的
|
|
120
|
+
远程 shell。
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
username: admin
|
|
124
|
+
password: 123456 (default — change it)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**在应用里**改:Settings → 账号(第一张卡片)接受新的用户名和/或新密码,立即生效,并写入
|
|
128
|
+
`<DATA_DIR>/credentials.json`(0600,scrypt 哈希 —— 磁盘上没有明文)。改密码会让其他所有会话失效;
|
|
129
|
+
发起改动的那台设备会拿到一个新 cookie,所以它保持登录。任何改动都需要当前密码,包括只改用户名。
|
|
130
|
+
|
|
131
|
+
`AGENTUS_USERNAME` / `AGENTUS_PASSWORD` / `AGENTUS_PASSWORD_HASH` 仍可作为引导:它们是应用的起
|
|
132
|
+
点,一旦你在 账号 卡片里保存了东西,从那以后就是文件说了算(和语音、主题一样的「设置文件 > env」顺序)。
|
|
133
|
+
只改密码会把当天的密码固化成哈希,这样 env 里的值就再也不能悄悄回来起作用。删掉 `credentials.json`
|
|
134
|
+
即可回退到环境变量。
|
|
135
|
+
|
|
136
|
+
账号管理住在 Settings → 账号(形状与 hermes-studio 的 AccountSettings 相同,去掉了多用户部分 —— 这个
|
|
137
|
+
驾驶舱只有一个运维者):
|
|
138
|
+
|
|
139
|
+
| 卡片 | 做什么 |
|
|
140
|
+
|---|---|
|
|
141
|
+
| **账号** | 改用户名或密码(各在一个小模态里,需要当前密码) |
|
|
142
|
+
| **登录会话** | 每个持有会话的浏览器和脚本:客户端、IP、最近活动、过期时间。可撤销一个,或「登出其他所有」。改密码会对其他所有会话一次性做同样的事。 |
|
|
143
|
+
| **登录失败锁定** | 登录限流器,可见:哪些 IP 被锁、锁多久,带 解锁 / 全部解锁 —— 这样把自己锁在自己的驾驶舱外面是一键问题,而不是要重启 |
|
|
144
|
+
|
|
145
|
+
会话是无状态 cookie,所以服务端在 `<DATA_DIR>/sessions.json`(0600)维护一份索引。它对于显示是参考
|
|
146
|
+
性的,但对于「是否存在」是权威的:不在索引里、或被撤销的 cookie 会被拒绝 —— 这才让撤销和「登出所有
|
|
147
|
+
设备」是真的,并且它能撑过重启。从旧版本升级时,会一次性收编已经存在的 cookie,而不是把所有设备踢下线。
|
|
148
|
+
|
|
149
|
+
登录后面的端点:`POST /api/auth/credentials`、`GET /api/auth/sessions`、
|
|
150
|
+
`POST /api/auth/sessions/revoke`、`POST /api/auth/sessions/revoke-others`、
|
|
151
|
+
`GET|DELETE /api/auth/locked-ips`。
|
|
152
|
+
|
|
153
|
+
刻意存在两种凭证:
|
|
154
|
+
|
|
155
|
+
| 凭证 | 怎么传 | 谁用 |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| **session cookie** | `HttpOnly` + `SameSite=Lax`,HMAC-SHA256 签名,7d | 浏览器(由 `POST /api/auth/login` 下发) |
|
|
158
|
+
| **machine token** | `Authorization: Bearer *** 或 `?token=<t>` | 脚本、CI、启动器 —— 从 `<DATA_DIR>/auth.token`(0600)读 |
|
|
159
|
+
|
|
160
|
+
cookie 用 `HttpOnly` 而不是 `localStorage` 里的 token,并且它会顺带搭上 WebSocket 握手。`/api` 下的
|
|
161
|
+
一切都对匿名调用者关闭;应用外壳、它的静态资源、`/healthz` 和 `/api/auth/*` 保持开放,好让登录页本身
|
|
162
|
+
能加载。`scripts/*.mjs` 会自动读取机器 token(`scripts/lib/auth.mjs`)。
|
|
163
|
+
|
|
164
|
+
认证相关环境变量:
|
|
165
|
+
|
|
166
|
+
| 变量 | 默认值 | 含义 |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `AGENTUS_AUTH` | `on` | `off` = 完全不登录(本地折腾用;启动日志会骂你) |
|
|
169
|
+
| `AGENTUS_USERNAME` | `admin` | 运维者用户名 |
|
|
170
|
+
| `AGENTUS_PASSWORD` | `123456` | 运维者密码(env 里是明文) |
|
|
171
|
+
| `AGENTUS_PASSWORD_HASH` | — | `scrypt:<salt>:<hex>`;设置后优先于 `AGENTUS_PASSWORD` |
|
|
172
|
+
| `AGENTUS_SESSION_TTL_MS` | 604800000 (7d) | 会话有效期 |
|
|
173
|
+
| `AGENTUS_AUTH_SECRET` | `<DATA_DIR>/auth.secret` | cookie 签名密钥(0600,自动生成) |
|
|
174
|
+
| `AGENTUS_AUTH_TOKEN` | `<DATA_DIR>/auth.token` | 机器 token(0600,自动生成) |
|
|
175
|
+
| `AGENTUS_LOGIN_MAX_FAILS` / `AGENTUS_LOGIN_LOCK_MS` | `5` / `30000` | 按 IP 的暴力破解锁定 |
|
|
176
|
+
| `AGENTUS_BASIC_AUTH` | — | 在**所有东西**前面加一道 HTTP Basic 质询(包括 `/healthz` 和 WS 升级)。`user:pass` 两者都校验;`:pass`(或裸 `pass`)**只校验密码**并接受任意用户名。这是你在暴露隧道之前想要的那道外层锁;见「公网暴露」。 |
|
|
177
|
+
|
|
178
|
+
### 公网暴露(隧道 / 反向代理)
|
|
179
|
+
|
|
180
|
+
把 `AGENTUS_BASIC_AUTH=:pass` 放进服务端环境,然后把隧道指向 `<lan-ip>:8787`。这样你就有了两把独立
|
|
181
|
+
的锁 —— 边缘的 Basic,里面的运维者登录 —— 脚本依然能进(`curl -u :pass` 加上机器 token)。
|
|
182
|
+
|
|
183
|
+
这里推荐只用密码的形式(`:pass`,或裸 `pass`):HTTP Basic 总是会让浏览器问用户名(RFC 7617 传
|
|
184
|
+
`user:pass`),但一个单运维者的服务从中一无所获 —— 随便打一个名字、或者留空,都能过。如果你确实配了
|
|
185
|
+
用户名,那它也会被校验。
|
|
186
|
+
|
|
187
|
+
**选一道外层锁,不要两把都开:**
|
|
188
|
+
|
|
189
|
+
- **隧道自带的门**,如果它有的话。SakuraFrp 的 `auth_pass` 是 IP 级授权:首次访问显示一个小页面要访问
|
|
190
|
+
密码,之后记住该 IP,WebSocket 也能穿过它继续工作(两端实测过)。这一侧零配置;代价是机器想进就得
|
|
191
|
+
走那套额外的授权流程(`POST /v4/tunnel/auth`)。
|
|
192
|
+
- **`AGENTUS_BASIC_AUTH`**,当隧道没有门、是纯 TCP 转发,或者你想要可脚本化的访问(`curl -u :pass`)。
|
|
193
|
+
真正的 401 质询被每个浏览器、代理和 HTTP 客户端理解 —— 不像某些隧道的「访问鉴权」形式,它们返回 HTTP
|
|
194
|
+
**200** 加一个「授权你的 IP」页面,对 `curl` 不可见。
|
|
195
|
+
|
|
196
|
+
两个都开会连续弹三次密码,所以选一个。
|
|
197
|
+
|
|
198
|
+
关于这个应用前面的隧道,要知道两件事:
|
|
199
|
+
|
|
200
|
+
- **WebSocket 必须能穿过。** 驾驶舱的实时流跑在 `/ws` 上;一个会丢掉 `Upgrade` 的 HTTP/1.1 隧道会让你
|
|
201
|
+
得到「页面能加载、然后永远显示重连中」。已验证能穿过 SakuraFrp 的 TCP+自动 HTTPS 隧道。
|
|
202
|
+
- **在边缘终结 TLS。** 当请求带着 `X-Forwarded-Proto: https` 到达时,session cookie 会自动以 `Secure`
|
|
203
|
+
下发(在同一隧道上实测过),所以无需配置。你 LAN 内的最后一跳仍然是纯 HTTP。
|
|
204
|
+
|
|
205
|
+
#### TLS:刻意做成第二个监听器
|
|
206
|
+
|
|
207
|
+
纯 TCP 隧道(SakuraFrp、safe-nat、`ssh -L`、大多数 frp 配置)只转发字节 —— 它**不会**替你终结 TLS。
|
|
208
|
+
把它指向 `:8787`,运维者的密码和 session cookie 就会以明文穿过互联网。所以服务端可以自己讲 TLS:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
scripts/make-cert.sh # self-signed, SAN = the name you actually type
|
|
212
|
+
# -> $AGENTUS_DATA/tls/{cert.pem,key.pem} (0600, git-ignored) — ~/.agentus/tls by default
|
|
213
|
+
# boot log then says: https://0.0.0.0:8443 (self-signed …) ; point a tunnel at THIS port
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
| 端口 | 讲什么 | 给谁 |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `AGENTUS_PORT` (8787) | 纯 HTTP | LAN、loopback、`curl`、脚本 —— 无证书警告,CI 不变 |
|
|
219
|
+
| `AGENTUS_TLS_PORT` (8443) | HTTPS(自签名) | **隧道** —— 加密公网那一跳 |
|
|
220
|
+
|
|
221
|
+
同一套 handler,同一套路由,同一套认证;只有 socket 不同。两个监听器胜过另外两种做法:把单端口变成
|
|
222
|
+
HTTPS 会把证书警告挡在你自己 LAN 的使用前面(并弄坏 `curl` 脚本),而在一个端口上嗅探首字节来同时服
|
|
223
|
+
务两者会掩盖一次访问到底有没有被加密。
|
|
224
|
+
|
|
225
|
+
**在你的设备上退掉那个警告。** 开着 TLS 时,监听器在 `/cert.crt` 提供自己的证书:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
https://<your-name>:<tls-port>/cert.crt # iOS: opens the profile installer directly
|
|
229
|
+
# Android: downloads it → Settings → Security → CA certificate
|
|
230
|
+
curl --cacert <AGENTUS_DATA>/tls/cert.pem https://<your-name>:<tls-port>/healthz # scripts, no -k
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
装成受信任的根证书后(iOS 还需要 *关于 → 证书信任设置 → 启用*),浏览器就不再追问,地址栏也就干净了。
|
|
234
|
+
它是公开材料 —— 私钥永远不离开 `<AGENTUS_DATA>/tls/`。
|
|
235
|
+
|
|
236
|
+
证书是**刻意自签名**的 —— 对一个未注册的域名或裸 IP,没有 CA 会签发,所以浏览器每台设备显示一次「不
|
|
237
|
+
是私密连接 → 继续」,而装上签发者(`/cert.crt`,见上)连这个都能退掉。把 SAN 保持为你实际输入的名字
|
|
238
|
+
(**DDNS 名**,而不是公网 IP:动态 IP 会需要重签,而名字不匹配会多一个警告)。
|
|
239
|
+
|
|
240
|
+
`make-cert.sh` 刻意签发**两张**证书:一张 10 年根证书(`ca.pem`,设备装的就是它)和一张 390 天的叶子
|
|
241
|
+
证书(`cert.pem`,监听器提供的就是它)。Apple 把*服务器证书*有效期上限定在 398 天 —— 一张 10 年的自
|
|
242
|
+
签名叶子证书恰恰是 iOS 在用户走完那套安装流程*之后*会拒绝的东西。根证书不是服务器证书,所以可以活得
|
|
243
|
+
久;轮换叶子证书(`--leaf-only`)永远不碰已经装好的设备。
|
|
244
|
+
|
|
245
|
+
`npm run tls-smoke`(在 CI 里,真 socket)守着这一切:一个做校验的客户端必须被*拒绝*(这才证明该端口
|
|
246
|
+
真的是 TLS),叶子证书必须能链到根证书、且 `/cert.crt` 发出的就是它、并保持在 398 天上限内,API 登录
|
|
247
|
+
和 `wss://…/ws` 握手必须能在 TLS 上工作(升级 handler 绑定在两个监听器上 —— 这是最容易忘的地方),而
|
|
248
|
+
LAN 端口必须保持明文。
|
|
249
|
+
|
|
250
|
+
登出会在服务端撤销 session id,所以「登出」是结束会话,而不只是藏起 UI —— cookie 立刻失效。
|
|
251
|
+
|
|
252
|
+
## HERMES_HOME(一个 slot 读写哪个数据目录)
|
|
253
|
+
|
|
254
|
+
被拉起的 agent CLI 会继承服务端的环境,所以服务端**总是显式设置 `HERMES_HOME`**:这一行自己指定了
|
|
255
|
+
home 就用它,否则用默认值 —— **你真实的 `~/.hermes`**,也就是你的 gateway 和 Studio 打开的那个
|
|
256
|
+
`state.db`。这就是预期的生产设置:一个 slot 驱动你真实的 agent,带着你的配置、凭证、记忆和会话列表。
|
|
257
|
+
|
|
258
|
+
*继承来的* `HERMES_HOME` 会被忽略(一个由 Hermes 启动的 shell 会把自己的泄漏下去),所以一个 slot 往
|
|
259
|
+
哪写永远是你选的东西 —— 在那一行上,或在 `AGENTUS_HERMES_HOME` 里。
|
|
260
|
+
|
|
261
|
+
要让某个 slot 的数据单独放(干净的会话列表、不同的 profile、一次性的实验),在那一行的 HERMES_HOME 里
|
|
262
|
+
命名一个目录即可 —— 不需要权限。早期开发的**隔离护栏**已经去掉了:它当初直接拒绝真实 home,是因为
|
|
263
|
+
Hermes 内置的 SQLite 3.50.4(落在 WAL-reset 区间内)在两个写入者共享一个 WAL 文件时会损坏真实的
|
|
264
|
+
`state.db`(2026-10-02)。运行时现在链接的是 SQLite 3.53.1,而共享一个 home 本来也是 Hermes 自己整天
|
|
265
|
+
在做的事(gateway + Studio bridge),所以驾驶舱不再挡路。
|
|
266
|
+
|
|
267
|
+
指向自己 home 的一行不需要权限 —— 只要一个名字。当一行指向别处时还剩一个陷阱:一个全新的 home,其
|
|
268
|
+
`hindsight/config.json` 保持默认 profile 名 `"hermes"` 的话,会接上你的生产记忆守护进程,所以这种
|
|
269
|
+
情况下那一行会给出警告。
|
|
270
|
+
|
|
271
|
+
## 目前能做什么(M0 → M4)
|
|
272
|
+
|
|
273
|
+
- 多会话侧栏,**按工作目录分组** —— 运行中的会话和**已关闭的案例**(进程已退出的 transcript;
|
|
274
|
+
点击可重新拉起 + `loadSession` 恢复,并重新套用存下的 permission mode/effort),
|
|
275
|
+
两者都支持按标题 / 后端 / cwd 搜索
|
|
276
|
+
- **会话自己起名**:agent 自己的标题(ACP `session_info_update` —— Hermes 在回合开场里生成一个)被
|
|
277
|
+
原样采用,而一个从不发标题的后端仍然会得到一个由第一条 prompt 派生的名字(首行,去掉 markdown,
|
|
278
|
+
≤50 字符);菜单里的 **重新生成会话名** 会让 agent 根据它**当前**的内容给对话起名,跑在一次性的
|
|
279
|
+
`session/fork` 上,所以会话自己的 transcript 不受影响
|
|
280
|
+
- 流式渲染 `agent_message_chunk` / `agent_thought_chunk`、工具调用(按 `toolCallId` upsert,从不
|
|
281
|
+
追加)、plan 更新、用量。一个工具调用就是一行紧凑的行 —— 状态点、截断的标题、折叠箭头 —— 只有展开
|
|
282
|
+
时才显示完整名字、kind、status、input 和 output(二十个调用的 transcript 依然可读)
|
|
283
|
+
- 每会话的**上下文窗口仪表**,来自 ACP `usage_update`(在 65% / 85% 告警;当 agent 不上报窗口大小时
|
|
284
|
+
降级为只显示 used)以及一个每回合的 trace chip,显示这一回合实际用的 effort/mode
|
|
285
|
+
- 由 agent 自己的 `available_commands_update` 驱动的**斜杠命令面板**(过滤、↑/↓、Tab 接受、Esc)——
|
|
286
|
+
永远不是一份我们编造的命令列表
|
|
287
|
+
- **fork 一个会话**(ACP `session/fork`,一个 Hermes 确实提供的不稳定能力):agent 把父会话的上下文
|
|
288
|
+
复制进一个新会话,在这里它会作为一个自己的会话出现 —— 同样的工作区,同样的模式,此后彼此独立
|
|
289
|
+
- 权限卡片(允许 / 总是允许 / 拒绝 / 忽略),接到 ACP 的服务端→客户端 `requestPermission`,带一个待
|
|
290
|
+
处理计数 chip 和一个超时,这样会话不会卡死
|
|
291
|
+
- permission mode + 推理强度切换(`setSessionMode` / `setSessionConfigOption`),按会话持久化
|
|
292
|
+
- SQLite transcript,每会话单调的 `seq`;重连只重放尾部;长 transcript 向后**分页**(「加载更早」)
|
|
293
|
+
而不是截断
|
|
294
|
+
- 孤儿进程回收:每个子进程都是 detached 拉起并记录 pid,所以一个崩溃的服务端留下的残渣会在下次启动
|
|
295
|
+
时被杀掉
|
|
296
|
+
- 容忍离线:WS 重连带 outbox(断线期间的输入被排队,而不是被吞掉),离线应用外壳走 service worker
|
|
297
|
+
- PWA + 手机布局:抽屉式侧栏、拇指大小的控件、安全区 padding、16px 输入框(避免 iOS 缩放)、390px
|
|
298
|
+
下无横向溢出
|
|
299
|
+
- 一个头部带**两个**控件:这个会话在哪个目录里工作,以及工作区面板。原来放在那上面的所有东西都挪到了
|
|
300
|
+
prompt 旁边它该在的地方
|
|
301
|
+
- **工作区面板**(头部 ▤):一个只读文件浏览器(面包屑、大小、文本预览)和一个以会话工作区为根的
|
|
302
|
+
shell,两者都随 socket 一起被杀掉。Cwd 是每会话字段:一个*运行中*的 agent 保持它启动时的目录,面板
|
|
303
|
+
和下一次恢复则跟随你选的那个
|
|
304
|
+
- **回复按 markdown 渲染**(markdown-it + highlight.js,用 DOMPurify 和 `html: false` 消毒,这样
|
|
305
|
+
agent 的 `<script>` 依然是可见文本):标题、列表、表格、引用、带语言标签的围栏代码、复制按钮和语法
|
|
306
|
+
着色
|
|
307
|
+
- **agent 自己的旋钮,按 ACP 本意摆放**:模型按钮只印出模型名("qwen3.8-flash");它的列表按 provider
|
|
308
|
+
分组并可折叠(Hermes 提供 501 个模型 —— 平铺的列表没法用);思考深度只显示一个小刻度(七格,按级别
|
|
309
|
+
填充,颜色从灰到红),这样手机宽的一行也放得下发送按钮:一个思考深度按钮和一个模型按钮,各自读取
|
|
310
|
+
agent 公布的选项(`configOptions[].category`,回退到 option id)—— 当 agent 一个都不提供时则隐藏。
|
|
311
|
+
模型切换用 ACP `session/set_model`,Hermes 实现了它(我们在用的 SDK 没有给它类型,所以走通用的
|
|
312
|
+
request() 重载)
|
|
313
|
+
- **上下文窗口由你声明**:点用量那一行来设定仪表所依据的窗口,按会话、持久化。ACP 没有改变一个模型窗口
|
|
314
|
+
的方法(那是 provider 的属性 —— 也就是 `usage_update.size` 所报告的),所以这个数字只驱动仪表;切
|
|
315
|
+
模型才是真正的杠杆
|
|
316
|
+
- 读起来像聊天框而不是工具栏的输入区:附件(`+`)、设置(permission mode · 思考深度 · voice)和口述
|
|
317
|
+
都在输入框下的一行上,上下文/花费那一行是它*上方*的小字,回合运行中发送键变成停止键
|
|
318
|
+
- **附件**作为真实的 ACP content blocks:图片(`type: "image"`)、带文件名头内联的文本文件、链接作为
|
|
319
|
+
`resource_link`。只有文件名被持久化 —— 从不持久化字节
|
|
320
|
+
- **语音,双向,浏览器优先**:朗读任意回复(每条消息一个按钮加一个自动朗读开关、音色选择、语速),口述
|
|
321
|
+
一条 prompt(带实时中间词)。服务端端点是可选的、且只是一个代理(`AGENTUS_TTS_BASE_URL` /
|
|
322
|
+
`AGENTUS_STT_BASE_URL`);什么都不配时,浏览器来做这件事,UI 会这样说明
|
|
323
|
+
- **语音通话模式**(聊天头部里的 📞):一个与会话的全屏通话 —— 一个随真实音频起伏的 canvas 光球,每个
|
|
324
|
+
阶段一种颜色(listening / thinking / speaking / error,会向屏幕阅读器播报),回复在流式生成时逐句念
|
|
325
|
+
出,以及**真正抢过话语权的 barge-in**:在回复上说话会停掉我们的播放、*取消 agent 的回合*并把麦克风
|
|
326
|
+
交还。它的阈值就是通话本身上的旋钮(角落里的 ⚙):抢话灵敏度(0-100 %,越高越容易打断,默认
|
|
327
|
+
60 %)、抢话持续时间、说完停顿 和 最少字数(默认 3)—— 放在桌上的手机把自己的扬声器漏进自己的麦克风,
|
|
328
|
+
戴耳机则不会,所以这些是每套安装的设置(存在服务端,跨设备共享)而不是常量。`最少字数` 只把关自动
|
|
329
|
+
发送;点一下光球永远会发送。通话打开时它拥有语音权:同一回复的自动朗读会让位,而一次通话只念**这一
|
|
330
|
+
回合**的答案 —— 永远不是上一个问题的答案
|
|
331
|
+
- **设置页**(侧栏里的 ⚙):主题(浅色 / 深色 / 跟随系统,外加一个给整个驾驶舱重新上色的强调色)、语音
|
|
332
|
+
provider、endpoint 和 key、模型(从 endpoint 自己的列表里选)、热词表,以及每个方向一个测试按钮。运维
|
|
333
|
+
者选择的一切 —— 主题、语音端点、通话的阈值,以及对话偏好(朗读回复、音色、语速、语言、识别器、服务
|
|
334
|
+
端合成)—— 都是**store 的 `settings` 表里的一行**(`voice` / `theme` / `call` / `prefs`,每段一行),
|
|
335
|
+
不是它旁边的一个文件,也不是浏览器的 localStorage:设置从手机跟到笔记本,同一个实例的两个标签页也不
|
|
336
|
+
可能不一致。一个 pre-store 时代的 `settings.json` 会被导入一次并保留为 `.imported`;浏览器只保存每段
|
|
337
|
+
的一个副本,好让首帧就已经正确
|
|
338
|
+
- **百炼 (DashScope) 语音,一等公民**:经其推理 WebSocket 做流式识别
|
|
339
|
+
(`qwen-audio-3.1-asr-flash-streaming` —— 话一出口词就出现),经 OpenAI 兼容 chat 路由做批量识别
|
|
340
|
+
(`qwen3-asr-flash`),经 `SpeechSynthesizer` 做合成(`qwen-audio-3.0-tts-flash`,音色如
|
|
341
|
+
`longanhuan_v3.6`)。浏览器打不开那个 socket(握手需要 auth header),而 key 不能离开服务端,所以驾驶
|
|
342
|
+
舱拥有上游连接,浏览器通过 `/ws/asr` 转发 PCM
|
|
343
|
+
- **热词,固定的和动态的**:一份固定列表(`词=权重`,`50` = 超级热词)进入识别器的即时词表,外加上从
|
|
344
|
+
这个会话自己的 transcript 及其邻居里挖出的实体词 —— 合并、去重、封顶,并可在设置页预览
|
|
345
|
+
- 一个会话的口述带着**那个会话的上下文**(它最近几回合)作为识别器的偏置,这就是让产品名和标识符保持
|
|
346
|
+
完整的原因
|
|
347
|
+
- 不与读者较劲的流式:滚轮/触摸手势会立即脱离,新输出由一个「跳到最新」按钮宣告,而不是把视口猛拽
|
|
348
|
+
下去
|
|
349
|
+
- 工作区选择器:浏览服务端的目录(`GET /api/fs/dirs`,一次一层)或从你之前用过的工作目录里挑;同一个
|
|
350
|
+
选择器可以重新指向一个已有的会话
|
|
351
|
+
|
|
352
|
+
## 命名
|
|
353
|
+
|
|
354
|
+
UI 到处都说 **session**(侧栏、空状态、对话框)。「Slot」只是项目的名字 —— 运维者面对的概念是会话,而
|
|
355
|
+
产品很可能还会改名;界面里没有任何东西依赖这个比喻。
|
|
356
|
+
|
|
357
|
+
名字有两层:`title` 是侧栏显示的,`auto_title` 是它回退到的生成名。生成名(agent 的,或我们从第一条
|
|
358
|
+
prompt 派生的)永远不覆盖手写的名字;清除一次重命名会把它恢复;而一次明确的**重新生成会话名**是刻意
|
|
359
|
+
替换它。重新生成走 agent,不走我们自己的模型:我们 fork 会话,fork 从复制过来的历史里总结,然后 fork
|
|
360
|
+
被丢弃 —— 我们这一层依然不含智能(红线 D)。
|
|
361
|
+
|
|
362
|
+
会话操作住在一个每会话菜单上(⋯ / 右键 / 手机长按):重命名(原地,可逆 —— 清除它恢复生成名)、
|
|
363
|
+
**从当前对话重新生成名字**、fork、工作区、**导出**
|
|
364
|
+
(`GET /api/sessions/:id/export?format=md|json` —— 从我们自己持久化的 transcript 渲染,所以一个已归档
|
|
365
|
+
的会话导出时无需唤醒 agent;ACP 本身没有导出的概念)、复制 id、归档(= 关闭:进程退出,记录保留),
|
|
366
|
+
以及对于已归档的 slot,恢复或删除。
|
|
367
|
+
|
|
368
|
+
## 测试
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
npm run typecheck
|
|
372
|
+
npm run auth-smoke # 48 assertions: the lock, both credentials
|
|
373
|
+
npm run backend-smoke # 42: the backend registry, spawn env, health evidence
|
|
374
|
+
npm run workspace-smoke # 85: workspace field, file API, shell, attachments
|
|
375
|
+
npm run voice-smoke # 47: settings/theme guards, voice router, 百炼 shapes
|
|
376
|
+
npm run tls-smoke # the second listener, real sockets + real certs
|
|
377
|
+
npm run package-smoke # packs, installs into a throwaway prefix + HOME, boots
|
|
378
|
+
# that install, drives a mock turn, checks ~/.agentus
|
|
379
|
+
node scripts/smoke.mjs mock "hello" # end-to-end against the mock agent
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
每个测试都在一个临时端口上、用一个一次性数据目录启动自己的服务端,所以它们在哪都能跑(包括 CI):
|
|
383
|
+
|
|
384
|
+
- `auth-smoke` —— 匿名 REST/WS 拒绝、暴力破解锁定、cookie 签名与篡改检测、过期、经 cookie 的 WS、
|
|
385
|
+
机器 token、登出撤销、`AGENTUS_AUTH=off`。
|
|
386
|
+
- `workspace-smoke` —— 每会话工作区、只读文件 API、PTY shell、prompt 上的附件、语音端点护栏。
|
|
387
|
+
- `voice-smoke` —— 设置/主题契约(校验、掩码、0600 文件)、对着一个替身端点(OpenAI 兼容**和**百炼的
|
|
388
|
+
原生形状)的语音路由、热词合并与封顶,以及在一个空 `$HOME` 下的 `.env` 引导。
|
|
389
|
+
|
|
390
|
+
浏览器 QA 证据(63 轮,每轮都有 repro → 根因 → 修复 → 回归)在
|
|
391
|
+
[`m1-qa-log.md`](./m1-qa-log.md)。`mock` 后端按需触发额外路径:`[tool]`(权限流程)、`[think]`、
|
|
392
|
+
`[plan]`、`[slow]`(重连演练)、`[sink]`(回合中途子进程崩溃)。
|
|
393
|
+
|
|
394
|
+
## 许可证
|
|
395
|
+
|
|
396
|
+
Apache-2.0。关于 AionUi 灵感的致谢(只有想法 —— 没有拷贝代码)见 [NOTICE](./NOTICE)。
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Running a development instance
|
|
2
|
+
|
|
3
|
+
This doc answers: how do I start a dev instance of Agentus, and how is its code and data
|
|
4
|
+
kept away from the live one?
|
|
5
|
+
|
|
6
|
+
## The two-worktree model
|
|
7
|
+
|
|
8
|
+
Agentus is developed in **two git worktrees of the same repository**, running as two
|
|
9
|
+
instances:
|
|
10
|
+
|
|
11
|
+
| | live | dev |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| path | `worktrees/agentus` | `worktrees/agentus-dev` |
|
|
14
|
+
| HTTP port | `8787` | `8901` |
|
|
15
|
+
| TLS port | `8443` | disabled (`AGENTUS_TLS_PORT=0`) |
|
|
16
|
+
| data dir | `<tree>/packages/server/.data` | `/tmp/agentus-qa-account` |
|
|
17
|
+
| started by | `~/.hermes/cache/agentus/launch.py` | `bash scripts/dev.sh start` |
|
|
18
|
+
| watcher | no (one process, no reload) | yes (`tsx watch`) |
|
|
19
|
+
|
|
20
|
+
The live tree serves the operator's real sessions. The dev tree is where you edit; its
|
|
21
|
+
`tsx watch` reloads on save, which is convenient precisely because its sessions are
|
|
22
|
+
disposable. **Never edit the live tree while the operator is using it** — a reload there
|
|
23
|
+
kills his agent children (see [03-release.md](./03-release.md) for the release path and
|
|
24
|
+
[04-recover.md](./04-recover.md) for what to do if it happened).
|
|
25
|
+
|
|
26
|
+
First move of any round: check where you are.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git -C worktrees/agentus branch --show-current && git -C worktrees/agentus status --short
|
|
30
|
+
git -C worktrees/agentus-dev branch --show-current && git -C worktrees/agentus-dev status --short
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Edit whatever the **dev** tree has checked out. Use absolute paths when writing files —
|
|
34
|
+
the two trees have identical filenames, so nothing reminds you if you picked the wrong one.
|
|
35
|
+
|
|
36
|
+
## The dev launcher: `scripts/dev.sh`
|
|
37
|
+
|
|
38
|
+
Run from the dev worktree root:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bash scripts/dev.sh start # start :8901, wait for /healthz, re-point the mock backend row
|
|
42
|
+
bash scripts/dev.sh status # is :8901 up? prints the pid, the tree, and /healthz
|
|
43
|
+
bash scripts/dev.sh logs # follow the dev log (tail -f /tmp/agentus-dev.log)
|
|
44
|
+
bash scripts/dev.sh stop # stop the process group on :8901
|
|
45
|
+
bash scripts/dev.sh promote [<slug>] # release to live — see 03-release.md
|
|
46
|
+
bash scripts/dev.sh promote-log # follow /tmp/agentus-qa-account/promote.log
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`start` prints where everything went:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
[dev] pid=<n> port=8901 data=/tmp/agentus-qa-account log=/tmp/agentus-dev.log
|
|
53
|
+
[dev] http://127.0.0.1:8901 login scratch / scratch-pass-1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Defaults are overridable by `DEV_PORT`, `DEV_DATA`, `DEV_USER`, `DEV_PASS`, `DEV_LOG`.
|
|
57
|
+
|
|
58
|
+
Three things `start` does that matter:
|
|
59
|
+
|
|
60
|
+
- **It detaches** the server from the calling shell's process group (`detached: true`), so
|
|
61
|
+
it does not die with the terminal that launched it.
|
|
62
|
+
- **It strips inherited identity.** `dev.sh` reads the caller's environment and deletes
|
|
63
|
+
every `AGENTUS_*` / `AGENTSL*` / `DEV_*` key it did not set itself, printing
|
|
64
|
+
`[dev] dropped inherited <KEY>`. This exists because a dev watcher that inherits a live
|
|
65
|
+
identity pair will, on a reload, read the live port/data and reap the live agent
|
|
66
|
+
children. Keep that behaviour when you touch the script.
|
|
67
|
+
- **It re-points the `mock` backend row at this tree.** A backend row stores an absolute
|
|
68
|
+
path to the mock agent, seeded by whichever tree first created the store, so a dev
|
|
69
|
+
instance reusing a store can silently spawn the *live* tree's mock. `start` PATCHes the
|
|
70
|
+
row back to `$HERE/packages/server/mock/agent.mjs` each boot and prints
|
|
71
|
+
`[dev] re-pointing backend 'mock' at this tree (was …)` when it does.
|
|
72
|
+
|
|
73
|
+
## Data directories and how they resolve
|
|
74
|
+
|
|
75
|
+
The server resolves its data directory in three ordered steps, and the **boot log prints
|
|
76
|
+
which one won and why** — a directory change is the thing that would otherwise read as
|
|
77
|
+
"my sessions are gone":
|
|
78
|
+
|
|
79
|
+
| order | rule |
|
|
80
|
+
|---|---|
|
|
81
|
+
| 1 | `AGENTUS_DATA` when it is set |
|
|
82
|
+
| 2 | `~/.agentus` once it holds something, or when there is no repo-side data yet |
|
|
83
|
+
| 3 | `<repo>/packages/server/.data` — an existing checkout that predates the install layout |
|
|
84
|
+
|
|
85
|
+
For the worktrees, the outcome is fixed by the launchers rather than by the rules:
|
|
86
|
+
|
|
87
|
+
- **live** — `~/.hermes/cache/agentus/launch.py` pins `AGENTUS_DATA=<live>/packages/server/.data`
|
|
88
|
+
and `AGENTUS_PORT=8787`, and pops the inherited QA/DEV keys. The launcher owns its
|
|
89
|
+
identity; it never trusts `os.environ`.
|
|
90
|
+
- **dev** — `scripts/dev.sh` exports `AGENTUS_DATA=/tmp/agentus-qa-account` and
|
|
91
|
+
`AGENTUS_PORT=8901`.
|
|
92
|
+
|
|
93
|
+
`agentus --where` (or `AGENTUS_PRINT_PATHS=1`) prints the resolved paths and exits without
|
|
94
|
+
starting anything — the single source of truth for "where did my state go".
|
|
95
|
+
|
|
96
|
+
### Keeping a dev instance's data separate
|
|
97
|
+
|
|
98
|
+
The dev instance already points at `/tmp/agentus-qa-account`, so it shares nothing with
|
|
99
|
+
live. Two independent knobs if you need more:
|
|
100
|
+
|
|
101
|
+
- **The store** — override `AGENTUS_DATA` (as `dev.sh` does). A throwaway instance is one
|
|
102
|
+
line:
|
|
103
|
+
```bash
|
|
104
|
+
AGENTUS_PORT=8907 AGENTUS_TLS_PORT=0 AGENTUS_DATA=/tmp/<name> npx tsx packages/server/src/index.ts
|
|
105
|
+
```
|
|
106
|
+
Note the server reads `AGENTUS_PORT`, not bare `PORT` — and this box exports an ambient
|
|
107
|
+
`PORT=8648` (Hermes Studio), so a script that reads the ambient value starts a retry loop
|
|
108
|
+
against the wrong port.
|
|
109
|
+
- **The agent subprocess home** — see below.
|
|
110
|
+
|
|
111
|
+
## The mock agent backend
|
|
112
|
+
|
|
113
|
+
The dev instance runs against a MOCK ACP agent:
|
|
114
|
+
`packages/server/mock/agent.mjs`. It implements enough of the ACP agent surface
|
|
115
|
+
(`initialize` / `newSession` / `prompt` / `cancel` / `setSessionMode` + the permission
|
|
116
|
+
flow) to exercise the whole pipeline with fake streaming and no real model or token spend.
|
|
117
|
+
|
|
118
|
+
The mock switches behaviour on the prompt text (per turn) and on env vars (global
|
|
119
|
+
defaults). The ones QA relies on:
|
|
120
|
+
|
|
121
|
+
| trigger / env | effect |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `[think]` / `[think:N]` | stream N thought chunks before the answer |
|
|
124
|
+
| `[tools:N]` | N tool calls, each with a reasoning burst (the transcript-flood case) |
|
|
125
|
+
| `[tool]`, `[tool-diff]`, `[tool-hermes]` | a tool call + `requestPermission` (the approval surface) |
|
|
126
|
+
| `[table]` | a deliberately-unsorted table, a local file link and a relative image |
|
|
127
|
+
| `[slow]` | ~900 ms between chunks, so a socket can be dropped mid-turn |
|
|
128
|
+
| `[sink]` | exit the process mid-turn (the crash path) |
|
|
129
|
+
| `[plan]`, `[blocks]`, `[mcp]` | plan / structured-block / handshake-shape probes |
|
|
130
|
+
| `MOCK_FORGET=1` | answer like a real agent to a forgotten session (empty `load`) |
|
|
131
|
+
|
|
132
|
+
**For an ordinary prompt the mock emits no tool calls.** So a visual rule about tool cards
|
|
133
|
+
(a "short card is not stretched to the column" CSS rule, say) cannot be measured from a
|
|
134
|
+
real dev turn — either drive a `[tools:N]` turn or inject a fixture row with the real class
|
|
135
|
+
names and assert the injection count before and after. See [02-verify.md](./02-verify.md).
|
|
136
|
+
|
|
137
|
+
## Front-end hot reload
|
|
138
|
+
|
|
139
|
+
The web bundle is built by Vite (`packages/web`, `vite.config.ts`). The server serves the
|
|
140
|
+
**built** bundle at `packages/web/dist`, not the sources, so:
|
|
141
|
+
|
|
142
|
+
- for standalone front-end work: `npm run dev -w @agentus/web` (Vite dev server, `host: true`
|
|
143
|
+
so it is reachable over the LAN);
|
|
144
|
+
- for anything the dev server or a sweep must serve: **`npm run build`** first. A sweep run
|
|
145
|
+
after editing web sources but before a build measures the previous bundle and reports
|
|
146
|
+
the old behaviour as if the fix did nothing.
|
|
147
|
+
|
|
148
|
+
## Isolating a slot's agent subprocess (HERMES_HOME)
|
|
149
|
+
|
|
150
|
+
A spawned agent CLI inherits the server's environment, so the server **always sets
|
|
151
|
+
`HERMES_HOME` explicitly**: the backend row's own home if it names one, otherwise the
|
|
152
|
+
default — the operator's real `~/.hermes`. That default is intentional for the live
|
|
153
|
+
instance (a slot drives the real agent). For a dev/test slot that should not touch the
|
|
154
|
+
operator's data, name a directory in that row's HERMES_HOME (no permission gate exists
|
|
155
|
+
any more).
|
|
156
|
+
|
|
157
|
+
One trap when a row points at a fresh home: an empty home has no `config.yaml`, no
|
|
158
|
+
`auth.json` and no `.env`, so `session/new` fails and the cockpit session goes `error`
|
|
159
|
+
with zero messages — which looks exactly like a broken adapter. Copy **all three** files
|
|
160
|
+
from a working home:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
mkdir -p /tmp/<home> && cp ~/.hermes/{config.yaml,auth.json,.env} /tmp/<home>/
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Also note a fresh home whose `hindsight/config.json` keeps the default profile name
|
|
167
|
+
`"hermes"` would attach to the production memory daemon; the spawn builder warns when it
|
|
168
|
+
would. Details: [`05-agent-driven.md`](./05-agent-driven.md) and
|
|
169
|
+
[`docs/refs/`](../refs/).
|
|
170
|
+
|
|
171
|
+
## Reading the dev logs
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
bash scripts/dev.sh logs # tail -f /tmp/agentus-dev.log
|
|
175
|
+
bash scripts/dev.sh status # :8901 up? pid, tree, /healthz
|
|
176
|
+
curl -s --noproxy '*' http://127.0.0.1:8901/healthz # {"ok":true,...} + backend list
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The dev log is a history of many launches (the `tsx watch` restarts write to it too), so
|
|
180
|
+
it is not by itself evidence about the *current* process — for "did it restart?", read the
|
|
181
|
+
served pid and the group leader's argv, not the log (see [03-release.md](./03-release.md)
|
|
182
|
+
and [04-recover.md](./04-recover.md)).
|
|
183
|
+
|
|
184
|
+
Cross-links: [02-verify.md](./02-verify.md) for the suites and sweeps ·
|
|
185
|
+
[03-release.md](./03-release.md) for `promote` · [04-recover.md](./04-recover.md) for
|
|
186
|
+
ports, stores and orphaned processes.
|