Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

运维操作

本章涵盖日常运维任务:升级、凭据管理和服务管理。


升级

拉取最新源码并重新构建:

cd octos
git pull origin main
./scripts/local-tenant-deploy.sh --full   # Rebuilds and reinstalls

如果以服务方式运行,升级后需要重启:

# macOS(launchd 系统守护进程):
sudo launchctl unload /Library/LaunchDaemons/io.octos.serve.plist
sudo launchctl load /Library/LaunchDaemons/io.octos.serve.plist

# Linux (systemd):
sudo systemctl restart octos-serve

会话存储容量

会话以滚动 JSONL 分段存储,而不是单个无限增长的文件。当活跃文件达到 OCTOS_SESSION_SEGMENT_BYTES(默认 8 MiB)时,它会被封存为旁边的 <name>.segments/NNNNNN.jsonl,并新建一个活跃文件。普通加载会读取活跃文件,再按新到旧纳入尽可能多的封存分段,直到 OCTOS_SESSION_LOAD_BUDGET_BYTES(默认 32 MiB;0 = 不限)——超出预算的历史仍留在磁盘上,可通过全量加载和 /undo 访问。

内存规划:该预算限制单个驻留会话占用的文件字节;解析后的行开销约为文件大小的 1.5–3 倍,因此缓存 N 个长会话的进程最多需要约 N × 32 MiB × 3。内存受限的主机应调低预算(如 OCTOS_SESSION_LOAD_BUDGET_BYTES=16777216)或调小会话缓存(gateway.max_sessions)。

混布版本重叠(例如共享数据目录上的 Kubernetes 滚动升级):旧二进制看不到 .segments/,其 *.jsonl 扫描只会看到活跃文件,看起来像一个很短的会话。本版本写入的会话携带 schema 版本 2,旧版本构建会整体拒读——但仍绝不能让旧二进制改写(重命名、摘要)任何已滚动的会话:改写会用旧构建能读到的内容整个替换活跃文件。对更老的(schema 1)会话,旧版重写擦掉 sealed_segments 会让封存分段对加载不可见;在这个未点名的状态存续期间,改写会被拒绝、seal 也会拒绝替换未点名的分段——文件保留在磁盘上,但该会话在状态修复前不再滚动。

中断的封存(在封存一个分段与新建活跃文件之间崩溃或被杀)会把封存分段留在磁盘上,而没有(或只有空的)活跃文件。该状态在读路径自愈:下次加载该会话时会从封存分段重建缺失或为空的活跃文件;在网关上,切换到该会话名的 /new <name> 也以同样方式恢复缺失的活跃文件——会话接续历史,而不是从空白开始。无法读取的活跃文件原样保留,留给人工处理。主动清空历史仍是 /clear(或裸 /new)。


钥匙串集成

Octos 支持将 API 密钥存储在操作系统的密钥存储中,而不是以明文形式存放在配置文件的 JSON 中:macOS 使用钥匙串(Apple Silicon 上提供硬件级加密和操作系统级别的访问控制),Linux 使用 ~/.octos/secrets 下的 0600 文件,Windows 暂无密钥存储——请改用环境变量或明文 env_vars。下图展示的是 macOS 后端。

架构

                     +------------------------------+
  octos auth set-key |     macOS Keychain            |
  -----------------> |  (AES encrypted, per-user)    |
                     |                               |
                     |  service: "octos"             |
                     |  account: "OPENAI_API_KEY"    |
                     |  password: "sk-proj-abc..."   |
                     +---------------+--------------+
                                     | get_password()
  Profile JSON                       |
  +------------------+               v
  | env_vars: {      |   resolve_env_vars()
  |   "OPENAI_API_   |   if "keychain:" ->
  |    KEY":          |   lookup from Keychain
  |    "keychain:"   |   else -> use literal
  | }                |
  +------------------+               |
                                     v
                               Gateway process

解析链:配置文件中的 "keychain:" 标记触发钥匙串查找(3 秒超时)。如果钥匙串不可用,该密钥会被跳过并输出警告。

向后兼容:env_vars 中的字面值直接透传。无需迁移 – 可以按需逐个密钥切换到钥匙串。完全支持明文和钥匙串条目混合使用。

CLI 命令

# 为 SSH 会话解锁钥匙串(通过 SSH 使用 set-key 前必须执行)
octos auth unlock --password <login-password>
octos auth unlock                               # interactive prompt

# 将密钥存入钥匙串并更新配置文件使用 keychain 标记
octos auth set-key OPENAI_API_KEY sk-proj-abc123
octos auth set-key OPENAI_API_KEY              # interactive prompt

# 指定配置文件
octos auth set-key GEMINI_API_KEY AIzaSy... -p my-profile

# 列出所有密钥及其存储状态
octos auth keys
octos auth keys -p my-profile

# 从钥匙串移除并清理配置文件
octos auth remove-key OPENAI_API_KEY

钥匙串条目格式

  • Service:octos(所有条目使用相同常量)
  • Account:环境变量名(例如 OPENAI_API_KEY)
  • Password:实际的密钥值

验证方法:

security find-generic-password -s octos -a OPENAI_API_KEY -w

SSH 和无头服务器设置

macOS 钥匙串绑定到 GUI 登录会话。SSH 会话无法访问已锁定的钥匙串 – macOS 会尝试弹出对话框,在无头服务器上这会导致卡死。

为什么 SSH 默认无法访问:macOS securityd 按会话解锁钥匙串。GUI 会话的解锁不会自动传播到 SSH 会话。

解决方案:解锁钥匙串并禁用自动锁定。每次启动执行一次(或加入部署脚本):

ssh user@<host>

# 解锁钥匙串(需要登录密码)
octos auth unlock --password <login-password>

# 完成 -- 自动锁定已自动禁用。
# 钥匙串保持解锁状态直到重启。
# 自动登录会在重启时重新解锁。

或使用原生 security 命令:

# 解锁
security unlock-keychain -p '<password>' ~/Library/Keychains/login.keychain-db

# 禁用自动锁定计时器(防止空闲后重新锁定)
security set-keychain-settings ~/Library/Keychains/login.keychain-db

常见问题:

现象原因解决方法
“User interaction is not allowed”钥匙串已锁定(SSH 会话)octos auth unlock --password <pw>
钥匙串查找超时(3 秒)钥匙串已锁定(LaunchDaemon)启用自动登录,重启
“keychain marker found but no secret”密钥未存储或使用了错误的钥匙串解锁后重新执行 octos auth set-key
网关启动时卡住钥匙串查找阻塞更新到最新的 octos 二进制文件

安全性对比

威胁场景明文 JSON钥匙串
文件被窃取(备份、git、scp)所有密钥暴露只能看到 "keychain:" 标记
恶意软件读取磁盘简单文件读取即可获取密钥必须绕过操作系统钥匙串 ACL
机器上的其他用户文件权限有一定保护,root 可读按用户加密
进程内存转储密钥在环境变量中密钥仅短暂存在于内存中
意外日志输出配置文件 JSON 泄露密钥仅记录引用字符串

服务器部署建议

macOS 钥匙串是为桌面交互使用设计的。在无头服务器上,它会引入可靠性问题。请根据部署类型选择凭据存储方式:

部署场景推荐存储方式原因
开发者笔记本钥匙串("keychain:")GUI 会话保持钥匙串解锁;ACL 弹窗可以接受
自动登录 + GUI 的 Mac钥匙串("keychain:")如果通过屏幕共享批准过 ACL 对话框则可用
无头 Mac(仅 SSH)env_vars 或 launchd plist 中的明文最可靠;无解锁/ACL 依赖
Linux 服务器密钥存储(~/.octos/secrets 下的 0600 文件)文件存储无需解锁或 D-Bus;明文环境变量亦可
Windowsenv_vars 或环境变量中的明文暂无密钥存储(#2234)

为什么钥匙串在无头服务器上不可靠:

  1. 需要 macOS 登录密码 – 通过 SSH 解锁钥匙串需要用户的登录密码存储在某处,降低了安全收益。
  2. 重启/休眠后重新锁定 – 启动 octos serve 的 LaunchDaemon 在 GUI 登录之前运行,此时钥匙串处于锁定状态。
  3. 空闲超时后重新锁定 – 即使解锁后,macOS 也可能重新锁定。set-keychain-settings 的变通方案可能被 macOS 更新重置。
  4. ACL 弹窗阻断无头访问 – 如果二进制文件不是最初存储密钥的那个,macOS 可能弹出一个无法回答的 GUI 对话框。
  5. 会话隔离 – 从 SSH 解锁不会解锁 LaunchDaemon 会话的钥匙串,反之亦然。

服务器的明文设置:

{
  "env_vars": {
    "OPENAI_API_KEY": "sk-proj-abc123",
    "SMTP_PASSWORD": "xxxx xxxx xxxx xxxx",
    "SMTP_HOST": "smtp.gmail.com",
    "SMTP_PORT": "587",
    "SMTP_USERNAME": "user@gmail.com",
    "SMTP_FROM": "user@gmail.com"
  }
}

使用文件系统权限保护文件:

chmod 600 ~/.octos/profiles/*.json
sudo chmod 600 /Library/LaunchDaemons/io.octos.serve.plist

工作密钥(会话入口)

外部 CLI 或脚本代理不应持有你的仪表盘 bearer 令牌。工作密钥(work secret)是一种短生命周期凭证,只授予一个代理对单个会话的访问权,走会话入口 WebSocket 路由(/v1/session_ingress/ws/{session_id})。

签发一条(运维提示走 stderr,编码后的密钥走 stdout):

octos auth issue-work-secret \
  --session "dspfac:local:tui#coding" \
  --profile dspfac \
  --ttl 1h \
  --api-base-url http://127.0.0.1:50080
  • --ttl 接受 15m、1h、3600s 这类值(默认 1h)。
  • 对同一会话重新签发会替换早先的授权;授权以 SHA-256 哈希形式持久化在 serve 数据目录(默认 ~/.octos/work_secrets.json,令牌本身从不落盘)。
  • octos auth list-work-secrets 列出已记录的授权;octos auth revoke-work-secret '<secret>' 撤销一条。

访客解码密钥后连到 /v1/session_ingress/ws/<URL 编码后的会话 id>,带 Authorization: Bearer <token>。无法设置请求头的 WebSocket 客户端可退回 ?token=<token>;该形式已弃用且会被服务端记录。套接字使用普通 UI Protocol 帧,每个客户端请求前都会重验授权,授权被撤销、过期或被重签替换后以 1008 关闭。只接受限定在被授权会话内的方法。

完整走查(含最小 Python 客户端):docs/OCTOS_WORK_SECRET_SESSION_INGRESS.md。


服务管理

macOS (launchd)

部署脚本将 octos 安装为系统 LaunchDaemon,位于 /Library/LaunchDaemons/io.octos.serve.plist(因此登出后仍存活,并在 GUI 登录前启动)。使用 sudo 管理:

# 加载服务
sudo launchctl load /Library/LaunchDaemons/io.octos.serve.plist

# 卸载服务
sudo launchctl unload /Library/LaunchDaemons/io.octos.serve.plist

# 检查状态
sudo launchctl print system/io.octos.serve

如果服务需要环境变量(例如 SMTP 凭据),将其添加到 plist 中:

<key>EnvironmentVariables</key>
<dict>
    <key>SMTP_PASSWORD</key>
    <string>xxxx xxxx xxxx xxxx</string>
</dict>

日志位于 ~/.octos/serve.log。

Linux (systemd)

部署脚本会在 /etc/systemd/system/octos-serve.service 安装系统单元。使用 sudo 管理:

# 启动 / 停止 / 重启
sudo systemctl start octos-serve
sudo systemctl stop octos-serve
sudo systemctl restart octos-serve

# 设置开机自启
sudo systemctl enable octos-serve

# 查看状态和日志
sudo systemctl status octos-serve
sudo journalctl -u octos-serve -f

通过 server/shutdown 停止(本地 solo)

服务器有三种停止方式:前台运行 octos serve 的终端里按 Ctrl+C、平台服务管理器(上文的 launchd / systemd),以及此处介绍的 UI Protocol 方法 server/shutdown。

UI Protocol 客户端可以通过已认证的 WebSocket(/api/ui-protocol/ws)调用 server/shutdown 方法停止服务器。它的效果与 Ctrl+C 完全一致:连接排空、网关停止、进程退出。该调用是幂等的,停止动作在请求被处理后约 250 ms 触发,确认通常仍能赶在排空前送达客户端(出站背压下客户端可能错过确认,但停止照常发生)。

该方法只在本地部署(config.mode = "local")且开启 solo 登录(octos serve --solo / OCTOS_SOLO_LOGIN=1)时被接受,且仅限 HTTP serve(不带 --stdio 的 octos serve)。一次调用会停止整个进程,所有已连接客户端一起下线,其运行中的轮次一并取消。fleet/托管服务器与 --stdio serve 会以 invalid_request(-32600)携带 data.kind: "server_shutdown_unavailable" 拒绝该调用,什么都不停;session 级(session-ingress)连接则完全无法调用,只会收到不带 kind 的裸 invalid_request。注意本地 solo 的信任模型:solo serve 上,任何能打开 WebSocket 的本地进程——或白名单来源页面——都能停止服务器。