这是多用户模式的运维页:开什么、按什么顺序开、每个开关到底暴露了什么。背后的设计决策在 team-server-plan.md,这一页讲的是你要做的事。
下面所有东西默认都是关的。默认安装就是它一直以来的那个单用户工作台——同样的路由、同样的行为、同样的测试(INV-1)。不会因为你升级了就发生任何变化。
export OPENAI4S_TEAM_MODE=1
openai4s serve
打开后,浏览器入口是 /login,没有会话 cookie 就什么都不答。第一个账号在机器本地建:
openai4s user add alice --role admin
loopback CLI 按决策 D2 等同管理员——能读到宿主上 access-token 文件的人本来就拥有这台机器——它的动作以 cli 记账,而不是冒充某个真人账号。
绑定地址仍然是安全边界。 团队模式加的是账号,不是"可以暴露"。放在终结 TLS 的反向代理后面,或者走 SSH——见 security.md。明文 HTTP 上的口令,就是这张网上的口令。
会话属于创建它的人,可选地属于某个项目。project 可见性意味着项目成员可读,private 意味着只有属主。没有项目的会话按构造即为 private;完全没有归属行的会话——团队模式之前的历史、CLI 直跑、demo 播种——则是仅管理员可见。最后这条是刻意的 fail-closed:「我们不知道这是谁的」绝不能解析成「大家的」。
管理员读一个 private 会话会往审计日志写一行 admin_read_private。这就是管理员权限的全部代价,而且是每次查看一行而不是每个会话一行。
读权限不等于命名空间控制权。对于项目内可见的会话,所有改变 frame 状态的操作都仅限会话属主或管理员:turn 与 review、权限决定、plan、annotation 与 Artifact、分享、checkpoint、分支激活/Revert/恢复、删除,以及 Notebook 执行和生命周期控制。D4 可见性切换特意更严格:只有属主(不包括管理员)能决定是否让自己的 Session 在项目内可读。以 POST 表达的 Revert preview 仍是读取。按资源 id 写入以及在 body 中指定 frame 的上传也继承同一属主规则,因此改变 URL 形状不能把项目可见性变成写权限。释放交互式算力分配同样仅限属主或管理员;请求交互式算力更严格——仅管理员可做——因为调度器使用的是 daemon 身份与站点凭据。通过全局或 frame-scoped kernel 路由安装包也仅管理员可做:即使 frame 属于调用者,两条路由改动的都是实例共享的运行环境。
配额按用户或项目、按种类、按窗口设置:
curl -X PUT .../api/v1/team/quotas -d '{"scope":"user","scope_id":"...",
"kind":"llm_output_tokens","limit_amount":2000000,"window":"month"}'
只有存在真实执行点的 kind 才允许设置。一个没人查的限额比没有限额更糟,因为总会有人按它做计划。
OPENAI4S_DATA_ROOTS 是冒号分隔的目录白名单,而 D8 说了三种根:只读的 datasets 区、项目区、以及个人 scratch。策略就写在同一个值里:
export OPENAI4S_DATA_ROOTS=/lab/datasets=ro:/lab/scratch
=ro 让一个根对所有人只读,管理员也不例外——只读根的意义就在于每个分析都在读的参考数据不能漂。可写的根有一个固定的命名空间:每位成员的上传落在 <root>/users/<用户名>/,由身份算出而绝不来自请求;别人的 users/<name>/ 不可读——共享区仍然共享,scratch 是个人的。这是固定命名空间而不是猜测,于是"这是不是别的成员的区域"是一个关于路径的问题,而不是"叫 alice 的目录是人还是数据集"的问题。
路由是 GET /api/v1/files(列目录)、GET /api/v1/files/download 与 POST /api/v1/files/upload;只要配置了根,团队模式关闭时同样可用。未配置根时,裸列表返回 200 {"roots": [], "configured": false}——工作台每次加载都会问它,以决定是否显示团队文件——而带 ?path= 的列目录、下载与上传返回 404 {"code": "no_data_roots"}。
本机 Python/R Cell 会在 open(2) 层执行同一套属主边界,而不只依赖 HTTP/Host
API。OS 沙箱会隐藏整个 daemon 数据目录、可写 data root 中其他成员的
users/<name>,以及 sibling 或旧版遗留的 kernel 临时目录;只把当前工作区和
本 Cell 的私有临时目录开放写入,并精确只读开放本会话需要的 runtime、已授权
Skill sidecar、属主个人区和按会话划分且校验过的 Artifact 输入缓存。若
Seatbelt/bubblewrap 无法建立并通过自测,Cell 即使在
OPENAI4S_KERNEL_SANDBOX=auto 下也会被拒绝;off 同样会拒绝。第一步就是
exec_background 也走这套策略。
不要把 OPENAI4S_DATA_ROOTS 放进系统 canonical 临时目录,也不要让两者互相
包含:团队模式会拒绝这种重叠,因为重新开放共享数据也会同时开放嵌套的个人区。
请使用持久化的实验室路径。这套策略隔离的是 OpenAI4S 管理的会话与个人数据,
不是同一 Unix UID 下的通用敌对主机沙箱;daemon 账号 home 中其他普通文件仍可能
被 Cell 读取。成员彼此不可信时,必须使用不同 OS 身份或容器/VM。
团队模式加的是账号,并不会把每一个 daemon 级的面都变成按人的。有些事是对着整个实例做的,那就是运维的事,无论谁登录了:
llm_base_url,全员的流量就指向了写的人选的主机;/compute/jobs),它以 daemon 自己的 uid 执行 bash -c <command>——读也不给,因为一条作业行就是某个人敲下的命令;POST /orchestration/jobs)。local 以 daemon 身份、在 kernel 沙箱之外执行 argv;cluster 则用 daemon 的 Unix 身份和站点凭据调用调度器。OpenAI4S 没有经过鉴权的“浏览器成员 → 调度器账号”映射,因此两种 backend 都不能当作该成员自己的执行身份;POST /sessions/{id}/compute),它调用的是同一套 daemon 管理的调度器身份。会话属主可以释放一份已有的分配,但只有管理员可以请求分配;/kernel/install 或 /frames/{fid}/kernel/install 往所有内核共用的 venv 里装包、配置携带组凭据的连接器、往每位成员的 agent 都会加载的目录里发布 skill、重置常设权限规则,以及创建全局权限规则(成员可以为自己的会话、或自己参与的项目建规则)。同样的区分也适用于 Cell 内部。host.skills.edit、publish、delete 和
rollback 会改写之后的会话将执行的指令或 Python sidecar,因此在团队模式下,
即使调用者已加入当前 project,这些 Host 写操作也要求管理员。skills_edit
默认也从静默放行改为询问。这不取消成员主动调用的 HTTP project 控制:成员仍可回滚到
启用历史能证明由 Web 创作、且仅含配方的版本;重新启用含 kernel.py 的版本或
来源无法确认的旧 Host 历史需要管理员。
成员保留 UI 需要的每一项读取。完整清单在 openai4s/server/team_policy.py;不在其上的路由就是成员的。
会话要能跑在调度器上,必须先满足两件事:站点被描述过,且 daemon 接受 worker 拨回。
描述站点:写 <data_dir>/cluster.toml。profile 是用户唯一能看到的词汇——它们各自映射到的队列与服务等级只留在这个文件里(决策 D5、INV-2):
job_name_prefix = "openai4s"
[profiles.cpu-interactive]
cpus = 8
memory_mb = 32768
walltime_s = 14400
partition = "compute" # 永不离开这个文件
[profiles.gpu-interactive]
cpus = 16
memory_mb = 131072
gpus = 1
walltime_s = 14400
partition = "gpu"
qos = "interactive"
接受 worker:给出一个计算节点够得着的地址:
export OPENAI4S_WORKER_LISTEN=0.0.0.0:8761 # worker 拨进来的地方
export OPENAI4S_WORKER_ADVERTISE=head01.lab # 告诉它们去拨哪里
OPENAI4S_WORKER_LISTEN 是监听器开关本身。默认不开,因为在每一台永远不会跑集群作业的笔记本上开一个监听器,是攻击面而不是便利。凡是绑定地址不是计算节点解析得了的名字,就要设 OPENAI4S_WORKER_ADVERTISE——绑 0.0.0.0 是"从哪儿都收"的写法,而 0.0.0.0 不是任何东西拨得过去的地方。
保护那个端口的是 bootstrap 凭据,不是网络。worker 出示的是对 (allocation, epoch, rank, expires, nonce) 的 HMAC,用每 daemon 一份的密钥签名;网关在交换任何一个协议字节之前就校验并烧掉它——这条 socket 承载 Host RPC,先服务后检查的监听器,在"后"的那段时间里就是一个远程执行面。拒绝只说 "refused":过期、重放、伪造之间的差别,对猜的人就是一个 oracle。
凭据以 0600 文件传递,只把路径告诉调度器(INV-9)。作业的环境,任何能向调度器查询该作业的人都读得到,所以提交环境会直接拒绝凭据形状的变量名。
该通道本身是明文的,所以这个端口只能放在可信网络上。 凭据只把 worker 向 daemon 认证一次;它不把 daemon 向 worker 认证,握手之后也不加密、不做完整性保护——而同一个 socket 上接下来跑的正是 kernel 协议和 Host RPC:被执行的代码、它的输出、以及 host.* 调用的结果。因此集群网络上的中间人既能读到这些帧,也能在 worker 外拨时冒充 daemon。请把监听放在不存在这种中间人的网络上,或者做隧道。把 0.0.0.0:8761 理解为"计算节点可达",而不是"可以安全暴露";这个 socket 的服务端认证 TLS 尚未实现。
关于同一个端口的两条边界:同时处于握手中的连接最多 MAX_PENDING_HANDSHAKES(64)个,多余的立即关闭——因为线程是在校验凭据之前就分配的;握手超时是总时限,所以逐字节拖延的对端无法长期占住一个槽位。
集群会话占着真资源,所以它有两个时钟:闲置 TTL(默认 2 小时)与最大生命期(默认 48 小时)。worker 活着不等于用户在场——内核健康、socket 连着的会话,只要没人在里面跑过东西就是闲置的,而且仍占着别人在排队等的东西。只有用户的执行,或显式续期,才续租约。
是哪个时钟到点,决定了告诉用户什么:SESSION_IDLE_TIMEOUT 意思是"回来它还在";SESSION_MAX_LIFETIME_EXCEEDED 意思是"这一个到此为止,做什么都没用"。
恢复策略是 WORKSPACE_ONLY,而且明说。文件活下来是因为它们本来就在共享文件系统上;内核的内存活不下来——变量、import、三个 cell 之前有人设的随机种子。会话在新的 epoch 上继续,界面升起 KERNEL_STATE_LOST 横幅,因为悄悄重连之后产出的结果,看起来和那个已经丢掉的会话产出的结果一模一样(INV-11)。
CHECKPOINT 被声明且以 501 拒绝。真正实现它需要进程级快照,且集群也得支持;做一半会恢复一部分状态、悄悄丢掉其余——那是三种可能行为里最糟的一种。
成员可以经 PUT /api/v1/auth/me/llm-key 按 provider 提供自己的凭据。密钥进的是与其他凭据同一个 secret broker,数据库只留引用。没配置就是回落,所以什么都不设的成员照旧用组里的 key。
配置了却读不出来的密钥会拒绝该轮而不是回落。用户明确要求用自己的凭据,悄悄记到组里是一个他没有做过的决定。
daemon 默认绑 loopback,这也是推荐做法。从别处访问,两条受支持的路,按优先级:
ssh -N -L 8760:127.0.0.1:8760 you@lab-host。什么都不暴露,鉴权用你已有的 SSH 配置,也没有新组件要运维。OPENAI4S_HOST=127.0.0.1:它是 daemon 的绑定地址,不是额外白名单。代理必须把 Host 改写为确切的 loopback upstream,例如 127.0.0.1:8760。如果原样透传客户端的 Host,每个请求都会拿到 403 host not allowed。把 OPENAI4S_HOST 设成代理主机名可能使 daemon 绑定到 loopback 之外,不是放行该主机名的支持方式。还必须逐个写明浏览器的外部 origin(scheme、host,以及非默认端口),这样 CSRF 守卫才能把可信代理造成的不一致与其他网站区分开:bash
export OPENAI4S_TRUSTED_PROXY_ORIGINS=https://lab.example
多个精确 origin 用逗号分隔;末尾一个 / 会被规范化。不接受通配符、凭据、非根路径、query 或 fragment。不设置这个变量时仍是严格默认值:Origin 必须与后端的 Host 字面一致。只要配置中含有一个 https:// origin,团队登录、邀请兑换和登出的所有 cookie 都会带上 Secure;配置存在期间,浏览器应只通过这个公开 TLS origin 访问。
配置任意 trusted proxy origin,也会在这个 HTTP listener 上禁用 daemon access token 对应的 admin 等价 SERVICE_IDENTITY。这是刻意的 fail-closed:代理连接 127.0.0.1 后,公网 HTTPS 客户端与本地 CLI 的 TCP peer 完全相同,Origin、Host 或 X-Forwarded-* 都无法还原可信的进程来源。经代理管理时请用普通 admin 账户登录,且不要向代理转发 daemon access token;未配置 trusted proxy origin 时(本机直连或 SSH 隧道拓扑),access-token CLI 通路仍保留。若要让公开代理入口与该 CLI 通路并存,需要独立的本机管理传输,约定一个 forwarded header 并不能建立这个边界。
relay 不是跑实验室服务器的第三条路。 openai4s relay 与 openai4s share 是另一件事——单个会话的只读、已脱敏快照,经由 daemon 主动拨出的隧道发送(见 webshare.md)。relay 看得到明文,而 share 投影刻意不是一个登录面:它不带 cookie、没有写路由、也没有活的内核。把它指向团队部署,得到的是某一个会话的投影,而不是把工作台开出去。
要在外网用工作台本身,走第 1 或第 2 条。
openai4s doctor # 配置、凭据、内核
curl -s localhost:8760/api/v1/auth/status
集群不可用时 daemon 会打印原因而不是拒绝启动:cluster.toml 写坏了就降级为仅本机并把原因写到 stderr,worker 监听器绑不上也一样。运维在配置文件里打错一个字,不该把整个工作台给所有人搞停。