第 19 章:CLI、配置与控制协议
19.1 CLI 不是单纯的管理员客户端
cmd/multica/main.go 把同一个 multica 二进制定义成三种角色:
- 人的控制台:登录、切换 Workspace、管理 Issue、Agent、Squad、Autopilot、Skill 和 Runtime;
- Daemon 的进程管理器:启动、停止、重启、检查健康状态、查看日志和磁盘占用;
- Agent 的任务内工具箱:读取 Issue/Chat、发表评论、下载附件、按需 Checkout 仓库、记录 Squad 决策。
第三种角色最关键。Provider CLI 不需要理解 Multica 的数据库或 HTTP 路由,只要调用已经放进任务环境的 multica,就能以当前 Agent、当前 Task 和当前 Workspace 的身份回写控制平面。
因此 CLI 同时跨过两条边界:
- 用户 Shell → Server API;
- Agent 子进程 → Server API / 本机 Daemon。
这也是为什么它的配置回退、身份注入和错误分类比普通 CRUD CLI 更严格。
19.2 Cobra 命令树
根命令使用 Cobra,按用途分为三组。
| 分组 | 主要命令 | 责任 |
|---|---|---|
| Core | issue、project、label、property、agent、autopilot、workspace、repo、skill、squad、chat | 操作协作领域对象 |
| Runtime | daemon、runtime | 管理本机执行器与服务端 Runtime |
| Additional | auth、user、login、setup、attachment、config、update、version | 引导、身份、文件与运维 |
命令实现分散在 server/cmd/multica,共同复用 internal/cli 中的 HTTP Client、配置、输出、错误和更新逻辑。
根命令设置三个持久 Flag:
--server-url;--workspace-id;--profile。
另有 --debug 控制错误展示深度。默认只显示可行动的用户消息,Debug 才展开原始错误链。
19.3 同一个命令为什么既适合人又适合 Agent
以 Issue 为例,cmd_issue.go 不只提供 List/Get/Create/Update,还提供:
- Comment List/Add/Delete/Resolve;
- Subscriber List/Add/Remove;
- Runs 与 Run Messages;
- Usage;
- Rerun 与 Cancel Task;
- Search;
- Label、Metadata 和 Custom Property 子命令。
人可以在终端直接调用;Daemon 启动的 Agent 也可以在 Brief 指引下调用。差别不在命令实现,而在 newAPIClient 最终装入的 Token 与身份 Header。
这个设计把“Agent Tool”降成普通、可测试、可脚本化的 CLI,而不是为每个 Provider 单独实现一套 Tool Calling Schema。
19.4 Profile 是机器侧隔离单元
internal/cli/config.go 定义 CLIConfig。默认 Profile 存在:
~/.multica/config.json
命名 Profile 存在:
~/.multica/profiles/<name>/config.json
Profile 不只隔离 Server URL 和 Token,还隔离:
- 默认 Workspace;
- Daemon PID、日志和状态目录;
- Workspaces Root;
- Device/Runtime Display Name;
- 并发、轮询、心跳和超时;
- 自动更新策略;
- 本机 Backend 路径;
- Custom Runtime Profile 的可执行文件映射。
同一台共享服务器可以用多个 Profile 连接不同 Multica Deployment 或不同账号,又不会把 Daemon 状态和 Worktree 混在一起。
19.5 配置文件的写入语义
配置保存不是直接覆盖目标文件:
- 在同目录创建临时文件;
- 写入缩进 JSON;
- 关闭文件;
- 将权限设为
0600; - 原子 Rename 到目标路径;
- 任一步失败都清理临时文件。
目录权限是 0755,但包含 Token 的配置文件本身是 0600。原子替换避免进程崩溃时留下半截 JSON。
这解决的是本机一致性,不等同于系统 Keychain:Token 仍存在普通文件中,机器权限边界依然重要。
19.6 三层配置优先级
大部分全局选择遵循:
显式 Flag > 环境变量 > 当前 Profile 配置
例如 Server:
--server-url > MULTICA_SERVER_URL > config.server_url
Workspace:
--workspace-id > MULTICA_WORKSPACE_ID > config.workspace_id
Daemon 的细粒度参数再补一个内置默认:
Flag > Env > Config > Default
cmd_daemon.go 为字符串、正整数、正 Duration 和允许零值的 Agent Timeout 分别实现解析器,原因是“零”的语义不一致:
- Poll Interval 的零表示未设置,是非法持久值;
- Agent Timeout 的
0s表示明确禁用绝对墙钟上限; - 未持久化 Agent Timeout 必须用
nil表示,不能和0s混为一谈。
19.7 Setup、Login 与 Auth
cmd_setup.go 提供 Cloud 与 Self-host 两条引导路径,先建立 Server/App URL,再进入认证。
cmd_login.go 支持:
- Browser 登录;
mul_User PAT;mcn_Cloud Node PAT;- 登录后发现 Workspace;
- 首次无 Workspace 时引导 Web 创建;
- 选择并持久化默认 Workspace。
Browser Flow 生成随机 State 防 CSRF,在本机临时监听 Callback;拿到浏览器返回的 JWT 后,再通过 API 创建 PAT。JWT 是一次登录桥梁,Daemon 后续使用存储的 PAT,不长期依赖浏览器 Cookie。
auth status 与 auth logout 操作当前 Profile;Logout 删除存储 Token,而不是删除 Server 端所有 Token。
19.8 HTTP Client 的线协议
internal/cli/client.go 的 APIClient 为请求统一添加:
| Header | 来源 | 含义 |
|---|---|---|
| Authorization | Token | Bearer PAT、Task Token 等 |
| X-Workspace-ID | WorkspaceID | 当前租户边界 |
| X-Agent-ID | 任务环境 | 将写操作归因到 Agent |
| X-Task-ID | 任务环境 | 让 Server 校验 Agent 行为属于当前 Task |
| X-Client-Platform | 默认 cli | 客户端表面 |
| X-Client-Version | 构建版本 | 日志与指标分桶 |
| X-Client-OS | GOOS 规范化 | macos/windows/linux |
Client 封装 GET/POST/PATCH/DELETE、Multipart Upload 和 JSON Decode;所有非 2xx 响应都进入同一个有大小上限的 HTTPError 路径,避免无限读取错误 Body。
默认 HTTP Timeout 是 30 秒,可由 MULTICA_HTTP_TIMEOUT 使用 Go Duration 或秒数覆盖。上传等长操作使用至少 60 秒的 Context,而不是偷偷恢复为更短硬编码值。
19.9 错误是脚本契约的一部分
internal/cli/errors.go 把错误分成:
- 网络超时、DNS、拒绝连接、TLS、离线;
- 401、403、404、409、400/422、429、5xx;
- 未知错误。
并给脚本稳定退出码:
| Exit Code | 类别 |
|---|---|
| 1 | 通用错误 |
| 2 | 网络错误 |
| 3 | 认证或授权错误 |
| 4 | 未找到 |
| 5 | 参数/验证错误 |
普通输出隐藏无帮助的 Raw URL 与错误链;--debug 保留内部操作、类型与包装链。这样既改善人工体验,也允许自动化根据退出码分支。
19.10 任务内身份从哪里来
Daemon 在启动 Provider 子进程前注入任务上下文。核心变量包括:
MULTICA_SERVER_URL;MULTICA_WORKSPACE_ID;MULTICA_AGENT_ID;MULTICA_TASK_ID;MULTICA_TOKEN;MULTICA_DAEMON_PORT。
其中 MULTICA_TOKEN 在任务里应为 mat_ 前缀的 Task-scoped Token。Server 结合 Token、Agent ID、Task ID 和 Workspace 约束可执行操作及归因。
流程如下:
19.11 为什么必须禁止 PAT 回退
假设 Agent 子进程丢失 MULTICA_TOKEN,而 CLI 静默读取 ~/.multica/config.json:
- 操作会以登录用户而非 Agent 身份落库;
- 审计记录失真;
- Task 权限限制被扩大成用户权限;
- 若 Profile 默认 Workspace 不同,还可能写入错误租户。
所以 cmd_agent.go 采用 Fail-closed:
- 只要检测到 Agent ID、Task ID 或 Daemon Port;
- Token 与 Workspace 就不能回退全局配置;
- Token 必须具有
mat_前缀; - 缺失时直接报错。
MULTICA_SERVER_URL 单独存在不算任务信号,因为正常用户 Shell 也经常设置它。
19.12 环境变量被剥离后的 Marker 防线
子进程或 Shell Wrapper 可能清理部分环境变量。Daemon 因此还在 Workdir 写:
.multica/daemon_task_context.json
execenv/context.go 用 managed_by=multica-daemon-task 区分自己创建的 Marker;CLI 从当前目录逐层向上查找。
Marker 是“这里仍属于 Daemon Task”的非秘密信号,不包含可替代 Token 的授权能力。发现 Marker 但没有有效 mat_ Token 时,CLI 仍拒绝读取 PAT。
Daemon 还在 Workspaces Root 维护同类 Marker,覆盖子进程跑出具体 Workdir 的情况。若本地目录残留旧 Marker,错误会指出精确路径,让用户确认后移除,而不是模糊地说未认证。
19.13 Server API 与本机 Daemon API 是两条通道
多数命令通过 HTTPS 调 Server;repo checkout 是重要例外。
cmd_repo.go 要求 MULTICA_DAEMON_PORT,然后向:
http://127.0.0.1:<port>/repo/checkout
发送 Repo URL、Workspace、Workdir、Ref、Agent Name、Task ID 与 Checkout Mode。Daemon 才拥有:
- Bare Clone Cache;
- Worktree 锁;
- 路径合法性检查;
- 凭据与 Fetch 逻辑;
- Checkout 生命周期。
CLI 不自己运行一套平行 Git 逻辑,避免破坏第 8 章描述的并发和 GC 不变量。
19.14 Daemon 的本地健康控制面
daemon/health.go 在 Loopback 监听本地 HTTP:
/health:状态、PID、版本及启动信息;- Repo Checkout 入口;
- Readiness 所需的本地控制动作。
默认健康端口为 19514,命名 Profile 从 Profile 名稳定派生不同端口,降低同机多 Daemon 冲突。这个 Listener 是本机控制面,不是给公网暴露的 Server API。
daemon start 的后台父进程:
- 先探测是否已有实例;
- 检查 Profile PAT;
- 以同一可执行文件拉起
--foreground子进程; - 写 PID;
- 轮询健康端点,区分 starting 与 running;
- 捕获早退日志并给出重新登录或网络修复提示。
19.15 Daemon 命令的运维表面
multica daemon 提供:
- start / stop / restart;
- status;
- logs;
- probe-runtimes;
- disk-usage。
Start 参数覆盖:
- Device 与 Runtime Name;
- Poll/Heartbeat Interval;
- 最大并发;
- Agent Wall-clock Timeout;
- Codex Semantic Inactivity/Handshake Timeout;
- 自动更新与检查间隔。
probe-runtimes 允许 Desktop 在不正式启动 Daemon 时检查本机 Provider 能力。disk-usage 可按 Task、Workspace 或全部 Profile 聚合,专门服务 Worktree/Environment 排障。
19.16 Task 内的高价值命令
Issue 与评论
Agent 可以读取 Issue、子 Issue、评论、执行历史和用量,再添加或解决评论。它回写的是结构化协作记录,不依赖把结果从 stdout 猜回 Server。
Chat
cmd_chat.go 将渠道差异隐藏在 Server 后:
chat history看当前 Channel 顶层消息与 Thread 索引;chat thread展开当前或指定 Thread;- 只能读取当前 Task 被授权的会话范围。
Attachment
cmd_attachment.go 支持下载输入附件和上传输出文件。上传文件可绑定当前 Chat Task,Task 完成时自动附着到 Assistant Reply;返回 Markdown Snippet 让 Agent选择内联图像或文件卡片。
Squad Activity
Squad Leader 必须用 squad activity 记录 action、no_action 或 failed。它把“Leader 看过但决定不行动”变成可观测事实,避免调度器只能从缺少新任务猜测结果。
19.17 输出格式为什么默认不完全一致
列表型人工命令通常默认 Table;需要被 Agent 或脚本消费的命令多默认 JSON。例如 Chat Read 与 Attachment Upload 返回结构化字段,减少文本解析。
internal/cli/output.go 只提供小而稳定的 Table/JSON Primitive;每个命令仍明确声明自己的列或返回对象。
设计上的含义是:
- stderr 用于进度与人类提示;
- stdout 尽量只放可消费结果;
- JSON 模式适合 Agent 与 Shell Pipeline;
- Table 是展示层,不应被当稳定 Wire Schema。
19.18 ID 解析是 CLI 体验层,不是权限层
许多命令允许 ID、Slug、Name 或前缀。CLI 先列表/查询再解析:
- 唯一匹配继续;
- 多个匹配报歧义;
- 无匹配报 Not Found;
- 最终仍把完整 ID 发给 Server。
这种便利不能替代 Server 授权。即使客户端解析出跨 Workspace 对象,Handler 仍必须按 Workspace/Actor 重新验证。
19.19 自更新的供应链边界
cmd_update.go 与 internal/cli/update.go 支持:
- Homebrew 安装走
brew upgrade; - 其他安装从 GitHub Release 选择 OS/Arch Archive;
- 下载
checksums.txt; - 对目标 Archive 做 SHA-256 校验;
- Manifest 缺失、Asset 缺失或 Hash 不匹配均拒绝替换;
- Dev Build 不参加无人值守自动降级。
Daemon 可周期检查更新,但 Profile 可禁用或调整间隔。二进制替换使用临时 Artifact 与清理逻辑,启动时也会清除陈旧更新文件。
Checksum 证明下载内容与 Release Manifest 一致,但不等于完整的签名/透明日志供应链;生产环境仍应控制 GitHub、代理与安装渠道。
19.20 一条 CLI 调用的完整决策树
19.21 扩展一个命令时应检查什么
新增命令不只是挂到 Cobra:
- 明确它是人用、Agent 用,还是二者皆用;
- 复用
newAPIClient,不要绕过 Task Token Fail-closed; - 需要 Workspace 时调用统一解析与必填校验;
- 使用
cli.APIContext,长操作显式提高下限; - HTTP 错误走 Typed Error,不自行吞掉状态码;
- stdout/stderr 分工稳定;
- 提供 JSON 输出给脚本;
- Secret 参数优先支持 stdin/file,避免 Shell History;
- ID 便利解析后仍由 Server 重做授权;
- 若访问 Git/Worktree,委托本机 Daemon,而非另建实现;
- 为 Flag 优先级、任务内身份和错误码补测试;
- 更新根 Help 分组与文档。
19.22 本章结论
Multica CLI 的本质是跨交互平面与执行平面的协议适配器:
- 对人,它把 Server API 变成可发现的命令树;
- 对 Daemon,它管理常驻进程、Profile 和本机控制面;
- 对 Agent,它提供受 Task Token 约束的协作工具;
- 对系统,它用 Header、Exit Code、JSON 与 Fail-closed 规则形成稳定协议。
最重要的不变量是:任务内 CLI 可以继承任务身份,但绝不能在任务身份缺失时退回人的身份。
下一章进入这套系统如何部署、加固、观测、测试和发布。