浏览器访问与部署¶
Cortex 提供一个浏览器工作台——与桌面应用所包装的是同一个 SPA—— 无需安装任何东西,即可从任意浏览器访问。本页同时覆盖部署运行手册(构建 SPA、 在服务器上启用 Web UI 端点)和浏览器访问路径(经 Cloudflare Access 边缘登录访问)。
访问工作台有三条彼此独立的路径,它们的认证方式不同:
| 路径 | 使用者 | 认证方式 | 是否持有 clientToken? |
|---|---|---|---|
| 浏览器 + Access | 任何有浏览器的人 | Cloudflare Access 边缘登录(邮箱 / IdP),由服务器以 JWT 校验 | 否——浏览器从不接触令牌 |
| 浏览器 + token 登录 | 知道 token 的人 | 粘贴一次 token,换成 HttpOnly 会话 cookie | 否——只有服务器看到,且只看到一次 |
| 桌面(Tauri) | 已安装的桌面应用 | 存于操作系统密钥链的 Bearer x-cortex-token(即 clientToken) |
是 |
本页是浏览器访问 + 部署参考。安装原生桌面应用请见 桌面应用。
更新¶
帮助 → 检查更新(手机布局中为设置 → 检查更新)先向所连接的服务器查询最新发布,再将当前页面加载的正式构建入口脚本和样式表,与不使用浏览器缓存获取的同源 HTML 比较。浏览器不调用原生应用命令。开发页面或不支持的入口布局会显示已跳过页面检查;网络、认证或无效页面响应会显示检查失败,而不是“已是最新”。
服务器提示优先显示,桌面和手机浏览器都可操作。选择更新才会授权服务器安装并重启,期间连接会短暂中断。稍后隐藏提示,下次手动检查可重新打开。跳过此版本、禁用服务器检查和开发模式限制仍然有效。
服务器重新连接后,浏览器只复查页面资源。发现变化时提示刷新页面,不会自动刷新。刷新可能丢失未保存的页面状态,但不会停止服务器上运行的任务。选择稍后保留当前页面,下次检查仍可提示刷新。服务器更新和页面刷新分别确认。
内置的 Web UI 传输层¶
Web UI 传输层内置于 @cortex-agent/server,按需在 CORTEX_UI_HTTP 标志后加载。它携带
@trpc/server 与 jose,但二者是运行期惰性的:服务器仅通过一个只有在标志设置时才会到达
的动态 import 加载传输层。标志未设置时,传输层代码与 @trpc/server / jose 都不会进入运行时
模块图,因此仅用 Slack 或 TUI 的部署不承担任何 UI 负担——同时安装与升级仍是一条
npm install -g @cortex-agent/server。
启用后,该传输层:
- 以同源方式提供构建后的 SPA(
web/dist)与 tRPC API——同一端口、同一 origin, 浏览器加载index.html+ 资源并调用/trpc,无需任何跨域处理; - 在
/trpc暴露 tRPC API(查询/变更走 HTTP 批量,订阅走 SSE),进程内直连服务器的 domain 服务(无代理、无 sidecar); - 每个
/trpc请求都经过认证门控,三种凭据任一有效即可放行(见 认证)。
它与服务器运行在同一进程中,绑定到 127.0.0.1,设计上只应通过隧道对外暴露。
部署运行手册(源码 → 运行中的服务器)¶
以下步骤将你从全新检出带到一台提供浏览器工作台的 Cortex 服务器。
1. 前置条件¶
2. 构建 SPA¶
网页 SPA 构建产物位于 web/dist。在仓库根目录:
只构建 SPA 及其依赖:
web/dist 是一个纯静态产物(index.html + 带哈希的资源)。发布 @cortex-agent/server 包时,
web/dist 会被打包进包内(通过 prepack 步骤)并随包的 files 一同发布,因此已安装的服务器
无需额外构建即可提供 SPA。从源码检出时,你用上面的步骤自行构建它。
3. web/dist 从何处被提供¶
服务器按以下顺序解析 SPA 目录:
- 你显式传给它的目录;
CORTEX_UI_SPA_DIR环境变量;- 包根目录的
web/dist(存在于已安装/已发布的包中); - monorepo 的
web/dist(从源码检出运行时)。
如果你部署已安装的包,第 3 项开箱即用。如果你从仓库检出部署,第 4 项无需配置即可生效——
就地构建 web/dist 便会被自动提供。如果你把构建后的 SPA 部署到其他位置,用
CORTEX_UI_SPA_DIR 指向它。如果目录不存在(SPA 未构建),非 /trpc 路径返回 404 占位,
而 /trpc 仍可工作。
4. 启用 Web UI 端点¶
在服务器上运行:
该命令在 .env 中没有 CORTEX_CLIENT_TOKEN 时生成一个,设置端点变量,并把原生应用的 origin
加入 uiCorsOrigins。它可以重复运行,并报告配置是否发生了变化。如果想手工配置端点,在
~/.cortex/config/.env 中添加:
仅这一项就足以让浏览器打开工作台:token 登录默认开启,SPA 会询问 clientToken 并把它换成
一个会话(见用 token 登录的浏览器访问)。
CORTEX_UI_HTTP 接受 1、true、on 或 yes。未设置时,该端点——以及 Web UI 传输层
连同 @trpc/server / jose——都不会加载。
5. 重启守护进程以生效¶
守护进程在启动时读取 SPA 与环境变量,因此新的构建或环境变量更改需在重启后生效。守护进程尚未运行时用
cortex daemon 启动它;否则重启它:
重启会中断连接
平滑的 cortex daemon restart 会等到没有轮次在运行时才重启,并在服务器恢复前暂缓新的轮次,
但重启期间所有已连接的客户端都会断开。cortex daemon restart --hard 与 --force 立即重启,
会中断正在运行的智能体。请在系统空闲时重启;如果你的 Cortex 实例运行在审批策略之下,
请让重启走该审批步骤。
此时服务器仅在服务器主机上可通过 http://127.0.0.1:3004 访问。把它暴露给浏览器
是下一节的内容。
认证¶
/trpc 认证门在三种凭据中任一有效时放行请求,否则在 tRPC 运行前返回 401:
x-cortex-token头 等于服务器的clientToken——桌面 / 机器路径。优先检查, 使用常量时间比较。- 有效的
cortex_ui会话 cookie——token 登录的浏览器路径。浏览器通过POST /api/ui/login证明自己知道 token(仅一次)后由服务器签发。 - 有效的
Cf-Access-Jwt-Assertion头——Cloudflare Access 的浏览器路径。边缘在认证 用户后注入该 JWT,由服务器校验。
两条浏览器路径都不会把 clientToken 留在页面里:cookie 是 HttpOnly 的,SPA 自己的
JavaScript 读不到;Access JWT 则由边缘签发。
服务器针对你的 Cloudflare Access team-domain JWKS 校验 Access JWT,检查签名(仅 RS256 / ES256)、受众(AUD)标签、签发者与过期时间。如果服务器上未配置 Access, JWT 路径被禁用,认证门安全降级——未配置的 Access 路径绝不放行任何请求。
端口转发是有意只认 token 的
/forward(桌面外壳到服务器 loopback 服务的裸 TCP 隧道)只接受 x-cortex-token 头,
两种浏览器凭据都打不开它。浏览器会自动把 cookie 附加到 WebSocket 握手上,所以一旦在这里
接受会话 cookie,等于把本机每个 loopback 服务的裸 socket 交给每一个已登录页面。
经 Cloudflare Access 的浏览器访问¶
浏览器路径在一个专用 UI hostname 前放置 Cloudflare Access 边缘登录,用户在边缘用 邮箱 / IdP 登录,服务器只会看到已被验证的请求。
浏览器
│ https(Cloudflare Access:在边缘用邮箱 / IdP 登录)
▼
Cloudflare Tunnel (cortex-ui.example.com → 服务器 localhost:3004)
│ 边缘在每个请求上注入 Cf-Access-Jwt-Assertion
▼
agent-server Web UI 传输层 (校验 JWT;浏览器从不持有 clientToken)
├─ 提供 web/dist (同源 SPA)
└─ 提供 /trpc (同源真实数据,进程内直连)
1. 建立专用 UI hostname 与隧道路由¶
将一条 Cloudflare Tunnel 路由从一个新的公开 hostname(例如 cortex-ui.example.com)
指向服务器的 loopback 端点(http://127.0.0.1:3004,或你的 CORTEX_UI_PORT)。
与 cortex-client 端点使用不同的 hostname
你的远程 cortex-client 实例所连接的 hostname 绝不能放在 Cloudflare Access 之后
——Access 会挡住这些 WebSocket 客户端。始终给浏览器 UI 一个独立的 hostname,
只对这一个应用 Access。
2. 添加 Cloudflare Access 应用(账户侧运维)¶
在 Cloudflare Zero Trust 面板中,为该 UI hostname 创建一个 self-hosted Access 应用, 策略放行你的登录邮箱(或 IdP 群组)。这是在 Cloudflare 面板中执行的账户级操作,不在 Cortex 配置里。记下该应用的 AUD 标签——下一步需要用到。
3. 配置服务器校验 Access JWT¶
在服务器的 ~/.cortex/config/.env 中添加:
CORTEX_ACCESS_TEAM_DOMAIN=your-team # 裸 team 名,或 your-team.cloudflareaccess.com
CORTEX_ACCESS_AUD=<你的-Access-应用-AUD> # Access 应用的 AUD 标签
# CORTEX_ACCESS_CERTS_URL=... # 可选:覆盖推导出的 JWKS URL
服务器从 CORTEX_ACCESS_TEAM_DOMAIN 推导出签发者
(https://your-team.cloudflareaccess.com)与 JWKS URL
(https://your-team.cloudflareaccess.com/cdn-cgi/access/certs)。如果
CORTEX_ACCESS_TEAM_DOMAIN 或 CORTEX_ACCESS_AUD 有任一未设置,浏览器路径保持禁用
(仅令牌)。更改这些值后需重启守护进程。
4. 打开工作台¶
访问 https://cortex-ui.example.com。Cloudflare Access 要求你用邮箱 / IdP 登录;认证后,
边缘在每个请求上转发经校验的 Cf-Access-Jwt-Assertion,服务器提供同源 SPA,工作台加载
真实 tRPC 数据——无需令牌,无需本地安装。
用 token 登录的浏览器访问(不需要 Cloudflare)¶
如果你没有部署 Cloudflare Access——或者只是想从局域网里的笔记本、或经由 SSH 转发打开工作台
——浏览器可以通过粘贴一次服务器的 clientToken 来完成认证。
只要服务器开了 CORTEX_UI_HTTP=1,此功能默认开启。打开 UI 时,如果该浏览器还没有会话,
SPA 会先显示登录页而不是工作台:
浏览器 ──POST /api/ui/login {token}──▶ 服务器 (与 clientToken 做常量时间比较)
◀──Set-Cookie: cortex_ui=… ; HttpOnly; SameSite=Strict; Secure──
浏览器 ──之后每个 /trpc、/api 请求都自动携带该 cookie──▶ 服务器
它带来什么,代价是什么:
- token 只提交一次,随即被换成一个不透明的 32 字节会话 id。cookie 是
HttpOnly的, 页面 JavaScript——包括 XSS 注入的脚本——读不回去。 SameSite=Strict让其他站点发起的请求不带该会话。但同一父域名下的各个子域名仍算同站, Cloudflare Access 的凭据也会照常附带,所以服务器还会检查Origin:发往/trpc或/api路由 (包括登录)的GET、HEAD、OPTIONS以外的请求,若Origin既不是本服务器自己、也不在uiCorsOrigins中,一律返回 403;Origin: null总是被拒绝。不带Origin的请求(CLI、设备、 原生壳自身的 HTTP 客户端)只做凭据校验。"本服务器自己"指Origin的主机与请求的Hostheader 相同,因此服务器前面的反向代理必须原样转发Host(cloudflared 默认如此,除非设置了httpHostHeader;nginx 需要proxy_set_header Host $host)。- 服务器提供的文件以沙箱方式打开。 通过下载链接直接打开 HTML、XHTML、SVG 或 XML 文件时(工作区
文件、附件、看板素材和记忆文件都一样),服务器附带
Content-Security-Policy: sandbox allow-scripts与X-Content-Type-Options: nosniff,页面运行在不透明 origin 中,读不到会话,也无法以你的身份 调用 API。 - 会话能扛过守护进程重启:它们存放在
~/.cortex/data/ui-sessions.json(权限0600), 登录 30 天后过期,使用会话不会延长有效期。设置 → 高级里有退出此浏览器的登录按钮,桌面与 手机布局都有,会在服务端吊销该会话。 - 会话的权限严格弱于 token:它与 Cloudflare Access 登录的权限完全相同——tRPC 与
/api路由,永远不含/forward。 - 登录端点是公开的。它必须如此:没有任何凭据的浏览器得够得着它。token 错误会让调用方固定
等待 250 毫秒并被记入日志;而 token 本身是 32 字节随机数,在线爆破并不现实。这里有意不做
IP 封禁(在隧道后面所有请求看起来都来自
127.0.0.1)也不做全局锁定(那会让任何人把你自己 锁在门外)。
调整或关闭¶
CORTEX_UI_TOKEN_LOGIN=0 # 0、false、off 或 no:移除登录路由与 cookie 这条腿
CORTEX_UI_SESSION_TTL_DAYS=30 # 可选;会话有效期(天),默认 30
关闭之后,登录与登出路由不会挂载,会话 cookie 也不再被接受,只剩 header token 与(若已配置) Cloudflare Access 能放行请求。SPA 会在登录页如实说明这一点,而不是给出一个根本不可能成功的表单。
该用哪一条¶
当你希望由 IdP 管理访问、且访问者永远不该看到 token,或者 UI 直接暴露在公网上时, 在 UI hostname 前放 Cloudflare Access。当是你自己访问、且到服务器的通路本就私密或已加密 (隧道、VPN、Tailscale、SSH 转发、本机 loopback)时,用 token 登录。两者可以共存:同一台 服务器可以都开,每个请求按它携带的凭据被放行。
局域网上的纯 HTTP
只有当请求经由 HTTPS 到达(或来自 localhost)时,会话 cookie 才会带上 Secure。
对局域网地址的纯 HTTP 访问不能带——否则浏览器会直接丢弃这个 cookie——因此服务器会不带
Secure 签发并在日志里告警。此时该网络路径上的任何人都能读到这个会话。除可信局域网外,
请使用 HTTPS。
三条路径对照¶
三条路径都到达同一个 /trpc API 与同一个工作台,但在何处及如何认证上不同:
| 浏览器 + Access | 浏览器 + token 登录 | 桌面(Tauri) | |
|---|---|---|---|
| Hostname | 专用 UI hostname,位于 Cloudflare Access 之后 | 任何能到达该端口的 hostname | 一个不在 Access 之后的 hostname |
| 登录 | Cloudflare Access 边缘登录(邮箱 / IdP) | 粘贴一次 clientToken |
一次性输入 serverUrl + clientToken |
| 请求上的凭据 | Cf-Access-Jwt-Assertion(由边缘签发) |
cortex_ui 会话 cookie |
x-cortex-token 头 |
| 何处校验认证 | 服务器校验 JWT | 服务器校验会话 | 服务器校验令牌 |
clientToken 暴露 |
从不接触浏览器 | 输入一次,不在页面中留存 | 存于操作系统密钥链 |
能否打开 /forward |
否 | 否 | 是 |
| SPA origin | 同源(SPA + /trpc 同一主机) |
同源 | 直连 /trpc(启用 CORS) |
由于桌面应用发送 x-cortex-token,它必须通过一个不在 Cloudflare Access 之后的
hostname 连接(Access 会在边缘挡住 bearer 请求)。想要原生窗口、需要端口转发、且愿意在本地
存储令牌时,选桌面应用;需要让别人进来、而他们不该看到令牌时,选 Access;只是你自己的浏览器、
且到服务器的通路本就私密时,选 token 登录。
故障排查¶
浏览器出现 Cloudflare 登录循环或边缘 403
Access 应用策略未放行你的身份。检查 Cloudflare Zero Trust 面板中该 UI hostname 上 Access 应用的策略。
浏览器在边缘登录成功,但工作台显示 401 / 无数据
边缘已认证你,但服务器拒绝了 JWT。在服务器上确认 CORTEX_ACCESS_TEAM_DOMAIN 与你的
team 匹配、CORTEX_ACCESS_AUD 与 Access 应用的 AUD 标签完全一致,且设置后已重启守护
进程。若这些未设置,浏览器路径被禁用,每个浏览器请求都是 401。
登录页提示 token 登录已关闭
服务器上设置了 CORTEX_UI_TOKEN_LOGIN=0。要么移除它(token 登录默认开启)并重启,要么改经
Cloudflare Access 或桌面应用访问 UI。
token 正确,但登录没有反应 / 每次加载页面都要求重新登录
在守护进程日志中查找 ui-http cross-origin request refused。改写了 Host header 的反向代理会让
页面自己的 origin 看起来像外来的,于是服务器以 403 拒绝登录请求。让代理原样转发 Host(cloudflared
默认如此,除非设置了 httpHostHeader;nginx 需要 proxy_set_header Host $host)。
页面显示 Cortex UI not built
未找到 web/dist,因此除 /trpc 以外的路径都以这段文字返回 404,而 API 照常工作。构建 SPA
(pnpm --filter web run build)或将 CORTEX_UI_SPA_DIR 指向你构建的产物,然后重启。
UI hostname 完全不可达
- 确认已设置
CORTEX_UI_HTTP=1且添加后已重启守护进程。 - 在服务器上确认端点已启动:
curl http://127.0.0.1:3004/trpc应返回401 Unauthorized(而非连接拒绝)。 - 确认 Cloudflare Tunnel 正在运行且其路由指向正确端口。