浏览器 UI
为 cornus 服务器管理的工作负载和 Compose 项目提供的本地浏览器 UI,由 cornus web 启动。本指南讲述 UI 本身能做什么;命令页面是标志与用法的参考。
工作原理
cornus web 会启动内嵌 SolidJS 应用和客户端侧 backend-for-frontend (BFF)。UI 可显示工作负载生命周期与详情、Compose 项目及其 depends_on 图、客户端本地挂载、隧道与转发、请求两侧的 ingress 设置、配置文件、流式日志、并排放置文件浏览器与交互式 exec 终端的工作区,以及基于服务器内置可观测性存储的指标仪表板。BFF 还向客户端公开工作负载统计流。
Compose 结构、本地文件源和存活的后台 agent session 不属于服务器扁平化的 workload API,因此 BFF 在客户端运行。它与其他客户端命令一样使用当前选择的连接配置。项目视图使用传给该命令的 Compose 文件;若既未发现也未显式指定文件,服务器工作负载视图仍可使用,但项目视图为空。
UI 没有身份验证。在默认模式下,它只监听 loopback: --addr 必须使用 localhost 或 loopback IP literal;除非传入 --allow-non-loopback,否则通配地址和非 loopback 地址会被拒绝。使用 --publish-in-conduit 时,它完全不绑定 listener,只能通过 SOCKS5 conduit 访问;该 conduit 本身位于 loopback,因此无论哪种方式,无身份验证边界都保持不变。
浏览项目未提及的目录
文件浏览器的本地根目录来自 Compose 项目: 项目目录,以及解析到该目录之外的 bind mount 源。这覆盖了常见场景,但也仅限于此——一个临时目录、旁边的另一份检出、或者一块数据盘,对你来说完全可以浏览,对文件浏览器却不可见;而为此声明一个你本来并不需要的 bind mount,并不是一个好的表达方式。
--local-root 直接指定它们:
cornus web --local-root ~/scratch --local-root notes=~/wiki:ro每个值的格式是 [LABEL=]DIR[:ro]。标签是来源切换器中显示的名称,默认取目录的基名;:ro 让该根目录只读,并且这是写入端点上真实的拒绝,而不是提示。目录必须存在——它会在命令启动时解析为绝对路径并检查,而不是等到第一次列目录时才失败。
声明的根目录与其他根目录受同样的约束: 路径相对根目录解析且无法越出,文件系统根目录和内核伪文件系统会被直接拒绝 (切换器会说明哪个来源被拒绝以及原因) 。
它们在完全没有 Compose 项目时也可用——在没有 compose 文件的目录中执行 cornus web --local-root DIR,即可获得针对该目录的文件浏览器,并与服务器的工作负载视图并存。这里指定的根目录在切换器中排在 bind mount 源之前,因此在没有项目时,第一个 --local-root 就是默认值。
与 --publish-in-conduit 一起使用时,路径会在你的工作目录中解析,并以绝对路径发送给 background agent,因为 agent 自己的工作目录被冻结在别处。
绑定到主机之外
有时浏览器并不在运行 cornus web 的机器上: CLI 运行在跳板机上、运行在从另一个网络接口转发端口的 devcontainer 中,或者你想从同一 LAN 上的平板访问工作站。--allow-non-loopback 为这些场景允许通配或非 loopback 的 --addr:
cornus web --addr 0.0.0.0:8080 --allow-non-loopback --allow-host box.lan请理解这样交出去的是什么。UI 及其 MCP endpoint 没有身份验证,并针对你的连接配置公开 exec、常驻终端和文件写入,因此任何能访问该端口的人都能获得全部这些能力。请优先使用 SSH 隧道 (ssh -L 8080:127.0.0.1:8080 host) 或 --publish-in-conduit,两者都让 listener 保持仅限 loopback。只有当端口前面的网络本就受你信任时才使用该标志,并尽可能用 --addr 指明具体地址而不是绑定通配地址。
--allow-host 指定该 origin 响应的 Host header 值,从而让 DNS rebinding 防护保持开启: 传入你在浏览器中输入的 LAN IP 或主机名 (多个则重复该标志) 。以其他名称到达的请求会被拒绝并返回 421 Misdirected Request,同时指出可以接受它的标志。如果传入 --allow-non-loopback 却完全没有 --allow-host,则无法针对从未告知的名称执行该防护,因此防护会被整体关闭,并在启动时警告它已关闭。
--allow-non-loopback 和 --allow-host 与 --publish-in-conduit 互斥,后者不绑定任何本地 listener;已发布的 UI 响应 --publish-name。
UI 和工作负载共用一个浏览器 proxy 设置
当你通过 SOCKS5 conduit 访问 cornus 服务器的工作负载时——浏览器 proxy 设置为 cornus socks5 (或 cornus config set-context --conduit-mode socks5) 并解析 *.cornus.internal 名称——cornus web UI 是单独的 http://127.0.0.1:<port>,需要自己的浏览器设置。--publish-in-conduit 消除了这种分离:
cornus web --publish-in-conduit这会把 UI backend 交给 background agent;agent 在进程内 listener 上提供该 backend,并在共享 conduit 中以 cornus.internal (service-host suffix apex) 发布。随后,UI 通过访问工作负载的同一个 proxy 在 http://cornus.internal/ 响应——一个浏览器 proxy 设置即可访问两者。它不绑定本地端口,因此不会新增暴露;UI 的可达范围与 proxy 完全相同。
你不必在这里重复工作负载 session 的 conduit 设置。无论该 conduit 当初以何种设置启动,UI 都会加入 background agent 已经为这个连接运行的那个共享 SOCKS5 conduit;只有在没有可加入的 conduit 时,才回退到它自己解析出的设置。这正是"一个设置"的承诺在实践中成立的原因: 过去,解析出略有差异配置的 cornus web 会启动第二个 proxy,随后与第一个在同一个绑定地址上冲突——最常见的原因是 profile 启用了通过 conduit 访问 ingress,而本命令并没有对应的 flag。
发布名称跟随它所加入的 conduit: 若 --socks5-service-host-suffix 为 .demo.internal,UI 会在 http://demo.internal/ 响应,与工作负载相邻,而不是自成一个命名空间。--publish-name 仍然完全覆盖它。
命令保持在前台运行,并在退出 (或被终止) 时撤销该名称。如果 agent 重启,它会自动重新发布。
注意:
- 浏览器必须通过 proxy 执行 remote DNS (SOCKS5h),使
cornus.internal由 proxy 而不是本地解析——这与现有*.cornus.internal工作负载名称的要求相同。 - 发布名称只提供
http://,不提供https://。 - 工作负载 session 也应使用 socks5 conduit。如果它们以默认 port-forward 模式运行,UI 仍可解析,工作负载也仍可通过完整部署名称解析,但 Compose 短名称 (例如以
demo-web部署的服务对应的web.cornus.internal) 无法解析——这些 alias 只由 socks5 模式的工作负载 session 注册。 - 这里的
--conduit是固定设置,而不是选择 conduit: 指定地址或 suffix (--conduit socks5://.shared:1080、--conduit socks5://?suffix=.demo.internal) 表示"就用这些设置",这也是你有意另起一个独立 proxy、或在多个之中做出选择的方式。bare 的--conduit socks5、CORNUS_CONDUIT和 profile 都没有指定设置,因此与不带 flag 时一样会加入。 - 如果 agent 恰好为这个连接运行着多个共享 conduit,UI 会加入共享最多的那个并说明;启动时打印的 banner 行会指出它实际落在哪个地址上。
面向 agent 客户端的 MCP 端点
同一服务器还在 /.cornus/mcp 共同托管 MCP (Model Context Protocol) 服务器,使 agent 客户端——Zed 的 Agent panel、Claude Desktop 等——可以驱动 UI 所公开的同一组客户端侧能力: 列出并操作工作负载、读取依赖图和挂载、tail 日志、运行一次性命令,以及读取或写入 allow-list 中的 Compose/env/配置文件。它默认启用;传入 --no-mcp 可将其禁用。
MCP 工具只是 UI BFF 所用相同逻辑的轻量 adapter,因此两个界面不会产生偏差。流式传输仍仅限 UI: 交互式终端和实时日志/统计流不适合 MCP 的请求/响应模型,因此 MCP 提供有界的 logs_tail (最后 N 行) 和一次性的 exec_run (捕获 stdout/stderr/退出状态) 。
有一个工具方向相反。project_apply 会重新部署已加载的项目 (等价于 cornus compose ... up -d,因此标准 Compose 调谐和后台 agent 行为仍是正本) ,而 UI 中没有与之对应的操作。UI 是 CLI 的辅助界面,而不是它的第二个入口,因此重新部署属于你已经打开的那个终端;而驱动 MCP 的 agent 并没有这样的终端。
agent 还可以获取服务器的飞行记录,它回答的是事后“出了什么问题”,而不是“现在是什么状态”: activity_read 工具具有与 CLI 相同的 since/kind/unfinished 筛选器,另有 cornus://activity/unfinished 资源,即服务器及其 caretaker 已开始但从未完成的事项集合。资源形式最为实用: 客户端可以像文件一样附加它,因此当 agent 被问到行为异常的部署时,它一开始就知道上一台服务器在执行中途停止。两者都会随记录携带 liveInstance;没有它,当前服务进程自身未结束的生命周期会被读成崩溃。跟随 (cornus activity --follow) 与日志流出于同一原因仍仅限 CLI。
MCP 完整继承 UI 的威胁模型: 相同的 loopback / 无身份验证边界和相同的 DNS rebinding Host 防护——包括 --allow-non-loopback 放宽该边界时,此时 exec_run 和 file_write 会与 UI 一起暴露到网络上。使用 --publish-in-conduit 时,MCP 端点与 UI 发布在同一个 SOCKS5 conduit 中,这会像 UI 已经公开那样向 conduit 用户公开 file_write 和 exec_run——如果想缩小此处的影响范围,请使用 --no-mcp。
大多数 MCP 客户端通过 stdio 启动命令,而不是连接 HTTP URL。对于这类客户端,运行 cornus web --mcp-stdio;它通过 stdin/stdout 提供完全相同的工具界面,并且不绑定 HTTP listener。它复用与浏览器 UI 相同的连接配置和 Compose 标志;诊断信息发送到 stderr,因此不会破坏 stdout 上的 JSON-RPC 流。例如,在客户端中注册为:
{
"command": "cornus",
"args": ["web", "--mcp-stdio", "-f", "compose.yaml"]
}指标仪表板
Metrics 页面把服务器内置可观测性存储记录的内容绘制成图表。它不需要在工作负载中做任何埋点,也不需要在 UI 中做任何配置,但它需要这个存储,因此请以 --obs 启动服务器:
cornus serve --obs没有存储时,所有可观测性路由都返回 501。页面不会把它报告为错误,而是说明情况并指出所需的标志。
页面标题旁边的 Scope 开关用于选择仪表板的对象: Workloads (CPU、内存、内存上限、网络 I/O、磁盘 I/O、进程数) 或 Server (cornus 进程自身的 CPU、内存、Go 堆、goroutine 数、线程数、文件描述符数、网络 I/O,以及累计的构建数和部署数)。
一行过滤器用于收窄它:
| 控件 | 作用 |
|---|---|
| Range | 最近 15 分钟 / 1 小时 / 6 小时 / 24 小时。步长和刷新间隔随范围变化,因此 24 小时窗口不会每 15 秒重新读取一次。 |
| Workload | 把工作负载面板收窄到单个部署。仅在 Workloads 作用范围下出现。 |
每个面板都带有当前值、每个序列一条折线 (按副本、按 CPU 模式、按 I/O 方向),以及一个 Table 切换,用最新值 / 最小 / 最大 / 平均读取同一批序列 —— 因此没有任何数值只能靠悬停才能读到。悬停图表,或聚焦后使用方向键,会移动一条十字准线,读出该时刻所有序列的值。
累计计数器 (container_cpu_time、container_network_io、container_disk_io、process_cpu_time、cornus_server_network_io) 在浏览器中被微分为每秒速率,数值下降会被视为计数器重置,而不是负流量。
当前部署后端没有数据来源的指标族会被从仪表板中略去,而不是画成一张永远空白的图: Kubernetes 上的网络 I/O、磁盘 I/O、进程数与累积 CPU,以及其他后端上的瞬时 CPU。服务器会在 cornus observe status 中列出它们 (metrics.unsupported),筛选栏下方还有一行说明缺少哪些面板以及原因,因此缺失的图表依然是可以解释的。后端能够上报、只是还没有数据的指标会保留其面板,并显示"尚无任何来源上报过它",这正是存储本身给出的答复。
一个面板最多绘制 8 条序列 —— 这是调色板中能可靠区分的颜色数量 —— 并会写明省略了多少条。要看到其余序列,请收窄到单个工作负载,或改读表格。
作用范围会写进 URL (/metrics?workload=shop-web&range=6h),因此某个视图可以被链接和分享;无法识别的值会回落到默认值,而不是把页面清空。
从命令行获取同一份数据
仪表板查询的存储与 cornus observe metrics 相同。后者接受任意 PromQL,也能读到工作负载自己导出的指标;仪表板覆盖的是 Cornus 自动为你记录的那部分。
图表就在你所在的位置
同样的面板也会出现在它们所描述的对象旁边,于是那个最常见的问题 —— "这个东西忙不忙?" —— 不再需要专门跑一趟仪表板:
- Overview 上的每个项目区块和工作负载区块都带有一个双面板条带: 该标题之下所有对象最近一小时的 CPU 与内存。项目条带只覆盖该项目的部署,不含其他。All metrics → 会打开仪表板,并且已经收窄到同一范围。
- 工作负载自己的页面有一个 Metrics 区块 (该页面依次排布 Instances、Spec、Metrics、Logs),显示仅属于该部署的完整工作负载面板集,并带有自己的范围控件。
只有当服务器带有存储时才会出现这些条带;没有 --obs 时,Overview 保持原样,而不是在每个标题下长出一段说明。
这些视图里的 CPU 面板会把两种后端写法 (主机后端的 container_cpu_time 与 Kubernetes 的 container_cpu_usage) 合并到一张图上,因为它们是同一个量、同一个单位。完整仪表板则把它们保留为两个面板,空的那个会说明自己属于哪种后端。
已停止的部署仍然会画出它此前那段窗口的曲线。工作负载"此刻"如何是状态徽章的职责; 图表的职责是它做过什么。
Overview 上的 ingress 设置
Overview 在摘要卡片与各项目分区之间有一个 Ingress 分区。一个发往 ingress 主机的请求要经过两处相互独立的设置,因此该分区把两者都列出来: 服务器侧的设置不是客户端侧的设置,二者也无法互相推断。
Front door 是服务器所广告的内容 (GET /.cornus/v1/info):
| 行 | 含义 |
|---|---|
| Mode | 由真正的 ingress 控制器实现 ingress 时为 cluster; 由服务器用自己的 Host / 路径表路由到工作负载时为 emulated,这正是主机后端的做法。 |
| Base domain | CORNUS_INGRESS_DOMAIN。当某个部署的 Compose 块未指定主机时,可据此预测它将获得的主机名。 |
| Class | CORNUS_INGRESS_CLASS,会打在该服务器创建的每一个 Ingress 上。 |
| Listen | emulated 前端入口的绑定地址 (CORNUS_INGRESS_LISTEN)。未设置表示只能通过入口隧道到达。仅在前端入口为 emulated 时显示。 |
| Controller | native passthrough 会端口转发到的集群内控制器 Service。"none discovered" 正是客户端退回到自行模拟 ingress 的原因。 |
This client 是你自己的 conduit 如何实现 ingress,即 --ingress-conduit 设置,从后台 agent 读取:
| 行 | 含义 |
|---|---|
| Mode | native 直接隧穿到集群自己的控制器,由它完成路由并提供自己的 TLS; emulate 则运行一个经 conduit 到达的客户端侧反向代理。 |
| Domain | 派生 ingress 主机所用的后缀; "conduit default" 表示使用 conduit 自身的服务主机后缀。 |
| Controller | native 模式下的 passthrough 目标。 |
| Trust | emulate 模式下浏览器需要接受的内容: 各主机的证书,以及回退 CA (通常是 conduit 为本会话生成的)。正是这一行把浏览器的 TLS 报错变成可执行的动作。native 模式不显示任何内容,因为真正的控制器会提供自己的证书。 |
未广告前端入口的服务器,以及不路由任何 ingress 的客户端,都会明说这一点,而不是让分区消失。完全读不到设置的客户端也一样: 没有后台 agent 在运行就无从问起,此时该分区报告的是 unknown 而非 none。
两侧的配置方式参见 Ingress。
工作区
工作区是一块平铺式屏幕,容纳两种窗格: 一种是把本地挂载与运行中容器统一为同一命名空间的文件浏览器,另一种是工作负载上的交互式终端。无论窗格里装的是什么,切分、以标签页形式堆叠和重新排布的方式都一样,布局也能在重新加载后保留。
打开时只有一个文件浏览器窗格,停在挂载列表上。当你身处某个运行中的工作负载内部时,在终端中打开 (prefix t) 会在你正在浏览的目录里打开一个 shell — 是屏幕上的那个文件夹,而不是你在其中选中的某一行; 窗格的落点和其他新窗格一样,由你指向某个平铺块来决定。终端是一个站立的位置,而不是对某一行做的事,所以只有这条命令会忽略 打开 和 新建窗格 都会读取的选择。该命令始终列出,无法执行时会说明原因: 在挂载列表上还没有指定工作负载,本地文件夹没有可连接的容器,已停止的工作负载则会指名说明。
打开会把选中的行放进属于它自己的窗格: 文本文件用编辑器,图片用查看器,文件夹则是另一份列表。这三者是同一个命令; 命令面板会写出它将打开的那一行,当这一行是文件夹时末尾带一条斜杠 (Open "logs/"…)。Ctrl+Enter (Mac 上是 Cmd+Enter) 无需前缀即可执行; 对文件来说,在行上按 Enter、双击、点击文件名同样可以。
无论走哪条路径,它都会点亮平铺块上的落点标记,问你这个窗格该放在哪里: 按 Space 作为当前平铺块上的标签页,按方向键 (或 hjkl) 切分并放在它旁边,按 Esc 取消。在文件夹上按不带修饰键的 Enter 仍然是就地进入它,修饰键表达的是“不在这里,而在我接下来指的地方”。只要身处挂载之中,打开就始终列出,无法执行时会说明原因: 没有选中任何行、选中了多行,或者这个文件编辑器和查看器都无法显示。
如果你每次给出的都是同一个答案,那就只说一次。设置 → 工作区 → 新窗格落点提供三个选项: 每次询问 (默认)、总是并排放置、总是作为标签页。后两个就是把上面那两个答案固定下来,于是每条创建窗格的命令都会跳过提示直接落位。无论选哪个,可达的布局都不会因此增加或减少 — 这个设置只决定采用提示本身给出的哪一个答案。切分窗格不受影响: 它的名字已经说明了自己做出的落点,它只问放在哪一条边。
工作区比屏幕更大
窗格的落点决定它放在哪里; 它的代价是另一个问题,而答案是: 一旦没有空间可让,就不再有代价。
只要还有余地,切分就是普通的切分: 窗格对半分开,工作区仍然铺满屏幕。当对半分开会让窗格的宽度低于 40rem、或高度低于 20rem — 大致就是一份列表或一个终端不再值得阅读的临界点 — 切分便不再夺走空间,而开始创造空间。切分便不再夺走空间,而开始创造空间,屏幕于是成为一扇窗,望向一个比它更宽 (或更高) 的工作区。
它所做的是一条分三步的规则: 工作区沿着你切分的那个轴乘以黄金比例; 已经打开的一切分享其中的三分之二,并且完全保持原有的比例; 新窗格得到剩下的三分之一。因此任何两个窗格之间的关系都不会改变 — 它们都以同样的微小倍率一起变大来腾出空间 — 而新窗格的大小由工作区决定,而不是由你恰好切分的那个窗格决定。如果这三分之一低于 40rem 的下限,新窗格就取下限。只有位于增长边缘的窗格会随之拉伸,其余的一切都停留在原处,大小也不变。
移动窗格也按同样的标准衡量,衡量的对象是它落到旁边的那个平铺块: 放到一个还有余裕的窗格旁边,就只是分享那片空间; 只有当对半分开会低于下限时,工作区才会增大来腾出位置。
两个下限并不相同,因为窗格不是正方形,屏幕也不是: 40rem 相当于一份列宽完整的列表,或一个 80 列的终端,而 20rem 大约是终端的 18 行。若对两个轴要求同一个数值,就等于索取任何普通显示器都给不出的高度。
因此工作区在用尽余地之前,表现得和任何平铺式窗口管理器一样,用尽之后才开始滚动。在 2560px 的显示器上,起初的几次切分都是对半分; 在 1080p 上,左右切分是对半分,上下切分的第一次也是,第二次才开始扩展; 在 1280px 的笔记本上,左右切分一开始就会扩展,因为它的一半本来就已经低于宽度下限。
因此工作区是可以滚动的,而且会跟随你: 让某个窗格获得焦点时,它所在的平铺块会以最小的滚动量完整地进入视野。在左侧打开的窗格同样会出现在屏幕上,而不会让整个布局看上去跳动一下。选择窗格 (prefix s) 会带着视野一起走: 方向键在平铺块之间移动时,工作区会平移,让被高亮的那一块保持在视线内,而焦点在你按下 Enter 之前一直留在原处。按 Esc 退出,视野会回到开始走动时的位置,所以四处看看不需要任何代价。
Tab 是这趟走动的另一条路线。方向键在平铺块之间移动,而 Tab 在窗格之间移动 —— 每一个窗格都会走到,顺序就是列表排列的顺序,也就是编号计数的顺序,走到末尾会回到开头,Shift+Tab 则往回走。因此它也会穿过一个平铺块叠放的那些 标签页,这是任何方向键都做不到的移动: 叠放的标签页共享屏幕上的同一个位置,没有哪个方向能把它们区分开。你可以什么都不用查,想按几次 Tab 就按几次; 若已经知道编号,直接按那个数字键也可以。
打开 设置 → 工作区 → 窗格选择器中的缩略地图 后,选择器会在列表上方画出整个工作区: 每个窗格一个矩形,形状和比例都与工作区实际的样子一致。你正在走过的矩形会被高亮,持有焦点的窗格戴着它那个填实的编号,而一个强调色的边框标出工作区有多少落在屏幕上、又落在哪里。在宽达数个屏幕的工作区里,只有这个边框会说明这件事。点击某个矩形即可前往那个窗格,与点击它对应的那一行相同。
带有多个标签页的平铺块会为每个标签页各标一个编号,因为它们共享屏幕上的同一个位置; 而且每个编号都是独立的点击目标,这是唯一能靠指点选中后台标签页的办法。编号会在矩形内换行,所以高一些的平铺块能放下好几行编号,而不是一行加一句抱歉。只有当行也用尽了,剩下的才会被省略号取代,所以一个单元格绝不会把平铺块的标签页数量说少。保留下来的是该平铺块当前所显示的那个标签页周围的编号,因此用 Tab 走过各个窗格时,移动的编号始终是看得见的。
它默认是关闭的,因为只有当工作区已经大过显示器时它才值得拥有: 在一个装得下的布局里,平铺块本就都在你眼前,它们的图不过是你已经看得见的东西的图。打开之后,至少有两个平铺块时地图才会出现,而只有当屏幕外确实有东西时边框才会出现。
固定窗格选择器之后,它就不再是一个模式。面板右上角的图钉 — 在按下之前一直是淡的 — 会把选择器常驻在屏幕一侧的侧栏里: 无论你是否要求过,它都列出全部窗格,标出你当前所在的窗格,点击某一行就前往那个窗格。它不遮挡任何东西: 工作区让出侧栏的宽度,而不是从它下面穿过去,因此平铺块会在剩下的区域里重新排布。prefix s 仍然和以前完全一样地走过各个平铺块 — 走动只是报告到那块已经在那里的面板上,在此期间借用它的标题和高亮,而 Enter 或 Esc 结束走动之后,面板依旧留在原处。
无论是否固定,选择器都停在阅读方向的起始侧 — 从左往右的语言里是左侧,从右往左的语言里是右侧 — 因此固定是把面板往外挪,而不是挪到屏幕的另一边。设置 → 工作区 → 侧栏位置 只为侧栏覆盖这一点: 自动 跟随语言,左 和 右 则直接指定。
容器内的传输如何进行取决于部署后端。 在主机后端 (Docker、containerd、bare) 上,运行中容器里的每一个路径都是直接读写的。在 kubernetes 上有两条路径,文件浏览器会在两者之间做选择:
- 工作负载所声明的卷内部的路径走它的 caretaker sidecar,caretaker 只拿到这些卷,别的都拿不到。这是首选路径: 它对应用镜像没有任何要求,报告的是真实的 errno,而同一个卷两个路径之间的复制根本不会离开 Pod。
- 其他任何路径——也就是容器镜像里的内容——都走在容器内运行的 tar,与
kubectl cp使用的机制相同。这是到达 caretaker 并不共享的 mount namespace 的唯一办法。
值得了解的后果是: tar 这条路径需要应用镜像里有 tar。distroless 镜像没有,因此其中位于镜像层的路径可以浏览 (列目录是一次 exec,它总是可用) 但无法传输,拒绝信息会说明这一点。卷支撑的路径在这样的镜像上照常工作,因为那条路径根本不碰镜像。caretaker 尚未连接的 Pod 会给出类似的拒绝,但原因不同; 消息会区分两者,而后者会在 Pod 起来之后自行消失。
复制和移动不使用侧栏。 prefix s 是选择器自己的问题,理应待在它常驻的位置; 而复制到另一个窗格和移动到另一个窗格是等着被回答的问题,因此它们会在屏幕的另一侧打开一张自己的卡片,并让侧栏保持原样。这张卡片只是一份目标清单: 没有缩略地图,没有标出焦点位置的记号,也没有活动状态徽章 — 传输是按名称、按种类、按哪些行变灰来选目标的,而且它有意不移动焦点。处于 working 或 needs you 状态的窗格仍会在自己的标签上说明这一点,标签本来就是干这个用的; 但在一份"文件送到哪里"的清单里,它读起来会像是在警告你别往那里送。未固定时,这些问题的样子和以前完全一样。
只有在放得下侧栏的地方才会出现这个图钉: 窗口至少 960px 宽,并且由鼠标而非手指操作。在手机或平板上,无论这个设置是什么,选择器都保持浮动,这样从桌面同步过来的偏好就不会让小屏幕损失一列。
要自己移动视野,可以使用触控板或滚动条; 在触摸屏上则用两根手指拖动。一根手指属于它下面的那个东西: 列表会滚动,终端会滚动,标签栏会横向滑动,分隔条会改变大小。两根手指是其他任何东西都不想要的手势,因此可以从任何位置平移工作区,包括从一个终端的正中间。当设置 → 工作区 → 窗格缩放开启时,捏合缩放依然有效: 手指保持间距不变就是平移,张开就是缩放。
有两组命令用于相对相邻窗格调整大小,快捷键沿用 tmux 的约定: prefix Alt+←/→ 用于变窄和变宽,prefix Alt+↑/↓ 用于变矮和变高。使用鼠标时,按住 Alt 拖动分隔条: 普通拖动在分隔条两侧的窗格之间交换空间,按住 Alt 拖动则只改变其中一侧,差额由工作区承担。
当布局变得失衡时 — 一侧切分了三层,另一侧空空如也 — Even horizontal (prefix H) 会把所有窗格排成等宽的一行,每个窗格、每个标签页、每个运行中的 shell 都原样保留,改变的只是它们周围的排布。如果均分屏幕后的宽度低于 40rem 的下限,这一行会按下限来组织,转而让工作区变宽; 因此“均等”绝不会变成“八个窗格各占屏幕的八分之一”。
工作区自己的右边界和下边界也是分隔条。其他分隔条都位于两个窗格之间,在它们之间交换空间; 这两条的另一侧空无一物,因此拖动它们改变的是工作区本身的大小 — 随之变大的只有紧贴该边界的窗格,这与切分使工作区扩展时的规则完全一致,往里数第二列的窗格不会移动。它们位于工作区的边缘而不是屏幕的边缘,所以一旦把它拖到视口之外,就需要滚动回去才能再把它拖回来。
设置 → 工作区 → 新窗格大小可以关闭这一行为。切分窗格是经典的 tmux 行为 — 平铺始终铺满屏幕,因此每新建一个窗格,你所在的那个就会被减半,无论因此变得多小,任何时候都不会滚动。默认值是扩展工作区。
工作目录是一种期望,而非保证
它作为 exec 的工作目录发送,docker、containerd、裸主机和 Incus 后端都会遵守。Kubernetes 无法表达工作目录 (PodExecOptions 没有该字段),因此在 Kubernetes 工作负载上打开的终端会从镜像指定的位置开始。
终端 shell 探测
在工作负载上打开终端时,shell 不是猜出来的,而是找出来的。BFF 会在运行中的容器里跑一次探测,连接到该镜像实际拥有的最佳交互 shell。于是带 bash 的镜像给你 bash,只有 busybox 的镜像也仍然给你一个 shell。
候选按下列顺序尝试,第一个存在的胜出:
- 工作负载自身
entrypoint:或command:所指的 shell (当它确实是 shell 时)——这是镜像作者的选择,也是唯一已有存在证据的候选; - 工作负载的
x-cornus-shells:列表 (参见 Compose 扩展); - 所选连接配置的
shells:列表; - 浏览器自己的列表,位于 设置 -> 终端 -> shell 候选。
这些列表是拼接而不是替换: 更具体的来源只把自己的条目提到前面,并不移除后备项。每个条目是一个命令字符串,而不是预先切分好的参数列表: /bin/busybox sh 是一个条目,切分方式与 Compose 切分 command: 相同。
浏览器的默认列表依次为 /bin/zsh、/usr/bin/zsh、/bin/bash、/usr/bin/bash、/bin/dash、/usr/bin/dash、/bin/ash、/usr/bin/ash、/bin/sh、/usr/bin/sh、/busybox/sh、/bin/busybox sh、/usr/bin/busybox sh。
当一个候选都不存在时 (distroless 或 scratch 镜像),窗格会说明这一点并询问要运行的命令,而不是以一条泛泛的连接错误告终。窗格会记住它最终采用的 shell,因此重新打开或刷新都不会再次探测。
镜像每缺少一个候选,探测就多花一次 exec 往返,并在第一个能启动的候选处停止: 一个启动起来的 shell 会一次性报告所有候选。结果按工作负载缓存 30 秒。
配置中的 shells: 字段被视为安全敏感项,因为它指定了一个会在你的工作负载内部执行的二进制文件。项目级 cornus-context.yaml 只有在 --trust-context-file 下才能提供它; 自动发现的文件中该字段会被剥除。
文件编辑
编辑器仅限于解析后的 Compose 文件、env 文件和客户端配置文件。任意路径和路径穿越写法都会被拒绝。
编辑 Compose 文件不会触发任何重新部署: 想要应用改动时,请自行运行 cornus compose up -d。UI 中没有应用按钮——它是 CLI 的辅助界面,而不是它的第二个入口。(agent 客户端可以通过 project_apply MCP 工具获得该操作。)