数据截至 (上游 commit e55b2a12c9a5)
Kortix (Suna) — 架构与原理
30 秒导读: Kortix 是一个开源的「AI 公司命令中心」。它把一家公司的组织方式写成代码——agent、技能、连接器、定时任务、机器规格全都躺在一个 git 仓库里的
kortix.yaml(v1 时代是kortix.toml,两种格式并存、YAML 优先)。你发一句话,平台开一台一次性云沙箱、在一条独立分支上让 agent 真的干活、提交、推送,产出以 change request 的形式等你审、你合并才进main。
1. 这是什么(零基础也能懂)
一句话定义: Kortix 是一套「把公司当代码库来运营」的平台——项目 = git 仓库 + 一份 kortix.yaml,session = 一条独立分支上的一次性云沙箱,agent 在沙箱里用 OpenCode 干活、提交、推送,产出通过 change request 由人审入 main。
1.1 它解决谁的什么问题
假设你想让 AI 帮公司做真事:每天早上汇总昨天的提交、收到 GitHub PR 就自动 review、Slack 里 @ 一下就出一份报告。
用聊天框做不到三件事:
| 做不到 | 为什么 | Kortix 的回答 |
|---|---|---|
| 让它真的动手 | 聊天框没有一台能装软件、跑命令的机器 | 每个 session 一台完整 Linux 沙箱 |
| 让它的配置可审计 | prompt 和集成藏在某个 SaaS 后台里 | 全部写进仓库里的 kortix.yaml,可 diff 可回滚 |
| 让它不闯祸 | agent 直接改生产、直接拿到你的 API key | 产出走 change request;凭据只在服务端铸造 |
1.2 用起来什么样
README 给的是三条命令(README.md:58-67):
# 1 · 装 CLI
curl -fsSL https://kortix.com/install | bash
# 2 · 生成项目骨架 —— 产出 kortix.yaml + agents / skills / 运行时配置
kortix init
# 3 · 发上去 —— 推仓库,并把整套东西在云上拉起来
kortix ship
拉起来之后的日常循环(README.md:71-75):
kortix sessions new --prompt "汇总本周提交,开一个 change request"
kortix cr ls # 看 agent 提了什么 —— 合并才算数
kortix chat # 在终端里跟某个 session 的 agent 对话
这三条命令在代码里对应 runInit(apps/cli/src/commands/init.ts:236)、runShip(apps/cli/src/commands/ship.ts:127)、runSessions(apps/cli/src/commands/sessions.ts:149)与 runCr(apps/cli/src/commands/cr.ts:43)。
1.3 那份声明长什么样
manifest 的开头就把自己定了性:「项目级配置的唯一真源,跟代码一起躺在 git 里」,并用 kortix_version 钉住 schema 版本(packages/starter/templates/base/kortix.yaml:1-15)。v2 是 YAML-only,agents 从 v1 的 [[agents]] 数组变成了一个 agents: map,而且只管治理(授权/沙箱/技能),agent 的行为(prompt/model)全部搬进各 agent 自己的 .kortix/opencode/agents/<name>.md。脚手架模板里能看到几类声明:
| 声明块 | 白话 | 例子(脚手架模板自带) |
|---|---|---|
sandbox: + sandbox.templates[] | 这个项目的机器长什么样 | 4 vCPU / 16 GiB / 50 GiB,从 .kortix/Dockerfile.ml 构建(注释示例,packages/starter/templates/base/kortix.yaml:44-51) |
opencode: | agent 运行时的配置目录在哪 | .kortix/opencode(packages/starter/templates/base/kortix.yaml:73-74) |
agents: | 每个 agent 被授予哪些连接器、哪些 Kortix 自身动作 | kortix agent connectors: all(:87-92);session-reviewer 只读(:103-107) |
triggers: | 什么事件自动开一个 session | 每日 03:00 的 harness-reflector cron(packages/starter/templates/base/kortix.yaml:121-147) |
v2 把 channels 从 manifest 里拿掉了——频道 ↔ agent 的路由改在仪表盘里现场管理;连接过的频道仍会以一条 provider: channel 的 connector 条目回写进文件(packages/starter/templates/base/kortix.yaml:178-183)。老项目里的 kortix.toml(v1)继续可用:读取时优先找同名 .yaml/.yml,找不到再回退 .toml(packages/manifest-schema/src/format.ts:37-46)。
1.4 一句话直觉
把 Kubernetes 的心智搬到「公司」上: kortix.yaml 是你的 manifest(期望状态),apps/api 是 control plane(把期望状态调和成真实资源),沙箱是 pod(一次性、可随时重建),而 main 分支是那份只能通过审核才能改的期望状态。
平台版本统一:根 VERSION 文件(当前 0.12.9)一次性钉住 API、前端、CLI、桌面端。
2. 顶层全景(它大概怎么转)
2.1 三层结构图
怎么读这张图: 从上往下是「谁发起 → 谁决策 → 谁干活」,每个方框是一层进程边界。要点只有一条——所有客户端只跟控制面说话,没有任何客户端直连沙箱。
┌─ 客户端(四个壳,同一套 HTTP API)──────────────────┐
│ apps/web(Next.js 仪表盘) apps/cli(kortix) │
│ apps/mobile(Expo) apps/desktop-electron │
└───────────────────────┬───────────────────────────┘
│ HTTPS /v1/*
┌───────────────────────▼───────────────────────────┐
│ 控制面 = apps/api 单体进程(Bun + Hono) │
│ 子服务全部挂在一个进程上,见 index.ts:758-969 │
│ 路由/模型 · 计费 · 平台 · 项目 · 沙箱代理 │
│ git 代理 · 连接器网关 · LLM 网关 · 隧道 · 频道 │
│ 市场 · 权限(IAM,库而非路由) │
└───────────────────────┬───────────────────────────┘
│ 供给 + 反向代理
┌───────────────────────▼───────────────────────────┐
│ 数据面 = 一次性云沙箱(每 session 一台) │
│ kortix-sandbox-agent-server(守护进程) │
│ └─ opencode(真正写代码/跑命令的 agent) │
└───────────────────────────────────────────────────┘
2.2 控制面挂了哪些子服务
apps/api/src/index.ts 是一个刻意的单体:package.json 自称 “Kortix API - Unified monolith combining router, billing, platform, cron, and daytona-proxy”(apps/api/package.json:4)。挂载清单集中在 index.ts:758-969,每个子服务一条 app.route:
| 挂载路径 | 子服务 | 一句话 | 讲透它的章节 |
|---|---|---|---|
/v1/router | router(index.ts:800) | 搜索 / LLM 兼容层 / 第三方转发 | 06 |
/v1/generation、/v1/usage | 生成取证与用量汇总(index.ts:812-813) | 单次网关调用 forensics / 账户用量 rollup | 06 |
/v1/llm、/internal/gateway、/v1/llm-gateway | mountLlmGateway(wire.ts:381) | 三种 LLM 网关形态一次挂全 | 06 |
/v1/billing | billingApp(index.ts:815) | 订阅、额度、Stripe webhook | 06 |
/v1/platform | platformApp(index.ts:829) | API key、沙箱版本、供给商 | 03 |
/v1/projects | projectsApp(index.ts:837) | 项目、session、触发器、CR、密钥 | 01 02 04 |
/v1/marketplace | marketplaceApp(index.ts:838) | 浏览 registry 目录 | 01 |
/v1/skills、/v1/runtime-assets | 技能与运行时资产(index.ts:848、:857) | 平台技能目录 / CLI 与托管技能分发 | 01 |
/v1/git | gitProxyApp(index.ts:864) | 全平台唯一的 git 客户端源 | 04 |
/v1/connectors | connectorApp(index.ts:874) | 所有工具调用的收口(原 /v1/executor) | 05 |
/v1/webhooks/*、/v1/channels/* | 频道与触发器(index.ts:877-896) | Slack / Teams / Telegram / 邮件入口 | 02 |
/v1/tunnel | tunnelApp(index.ts:962) | 云 agent 反向连回你本机 | 03 |
/v1/p | sandboxProxyApp(index.ts:969) | 沙箱反向代理(通配,必须最后挂) | 03 |
注意一个例外: IAM 不是一条挂载路由,而是被各路由 import 的库——公共入口是 authorize / assertAuthorized(apps/api/src/iam/index.ts:10-58),真正判定在 authorize.ts 这一个规范引擎里(apps/api/src/iam/authorize.ts:1-18 的文件头自述:它取代并删除了 engine-v2.ts、V1 策略引擎和 projects/access.ts 三份 并行实现,现在是唯一授权路径)。
2.3 数据面里有什么
沙箱不是一个裸容器,里面有一层自己的运行时:
entrypoint.sh以 PID 1 起,先确保/workspace真实存在再交棒给守护进程——因为供给商的 init 可能在容器起来之后删掉原目录(apps/sandbox/entrypoint.sh:1-12)。- 守护进程
kortix-sandbox-agent-server,自述是「OpenCode supervisor + Kortix API surface」(apps/kortix-sandbox-agent-server/package.json:4),入口main()(apps/kortix-sandbox-agent-server/src/main.ts:78)负责配 git 凭据助手、物化仓库、拉起 OpenCode、开端口代理。 - OpenCode 才是那个真正读写文件、跑命令的 agent 进程,由
createOpencodeSupervisor托管(apps/kortix-sandbox-agent-server/src/opencode.ts:1573)。
细节在 03 沙箱内部。
3. 部件一句话职责表
3.1 apps/* —— 十一个目录
工作区定义在 pnpm-workspace.yaml:1-4(apps/* + packages/* + tests)。
| 目录 | 干什么 | 由哪章讲透 |
|---|---|---|
apps/api | 控制面单体:所有 /v1/* 子服务、后台 worker、领导者选举 | 02 04 05 06 |
apps/web | Next.js 15 仪表盘 + 官网 + 文档源(apps/web/package.json:2) | 本页(不下钻) |
apps/cli | kortix 命令行——「在终端做到仪表盘能做的一切」(apps/cli/DESIGN.md:7-24) | 01 |
apps/mobile | Expo/React Native 客户端(apps/mobile/package.json:1-3) | 本页(不下钻) |
apps/desktop-electron | Electron 桌面壳,包住远程 web app | 本页(不下钻) |
apps/sandbox | 沙箱基础镜像:Dockerfile + entrypoint.sh | 03 |
apps/kortix-sandbox-agent-server | 沙箱内守护进程(OpenCode 监管 + 沙箱侧 API) | 03 |
apps/llm-gateway | 独立部署的网关进程(与 API 内嵌形态跑同一份管线) | 06 |
apps/kortix-app-runtime | 应用部署的 Go 运行时(Caddy + 构建脚本) | 本页(不下钻) |
apps/voice-agent | 语音会话的 LiveKit agent(livekit.toml) | 本页(不下钻) |
apps/whitelabel-demo | 白标演示壳 | 本页(不下钻) |
3.2 packages/* —— 十一个共享包
| 目录 | 干什么(取自各自 package.json 的 description) | 由哪章讲透 |
|---|---|---|
packages/manifest-schema | kortix.yaml 的规范 schema + 校验器,CLI 和后端共用一份 | 01 |
packages/db | Drizzle ORM schema 与客户端 | 02 |
packages/shared | 常量、沙箱渲染层/运行时指纹、运行时版本表 | 03 06 |
packages/llm-catalog | LLM 模型目录(models.dev 生成):网关支持的模型、托管模型清单与默认 | 06 |
packages/llm-gateway | LLM 管线本体:多传输、失败转移、熔断、预算、trace | 06 |
packages/sdk | 官方 TypeScript SDK:项目/会话生命周期 + agent 流式,统一在一个 Session 句柄后面 | 05 |
packages/executor-sdk | 已废弃——@kortix/sdk 连接器 API 的兼容适配层 | 05 |
packages/agent-tunnel | 云 agent ↔ 本机的隧道:relay、本地 agent、JSON-RPC、HMAC 签名 | 03(隧道本体)05(它作为连接器的那一面) |
packages/registry | 市场引擎,shadcn 兼容的 registry 格式 + 构建/解析/安装原语 | 01 |
packages/starter | 项目脚手架模板 + 加载器,kortix init 和后端建仓路径共用 | 01 |
packages/api-contract | API 线格式契约:Zod schema + 推导类型,SDK/web/mobile 共用一份真源 | 本页(不下钻) |
apps/api 通过 workspace:* 依赖其中八个(apps/api/package.json:25-32):api-contract、db、llm-catalog、llm-gateway、manifest-schema、registry、shared、starter;隧道包写法不同,单列在 :37("agent-tunnel": "workspace:@kortix/agent-tunnel@*")。executor-sdk 不被控制面依赖——它是给沙箱侧用的兼容层,新代码用 packages/sdk。共享包被两侧同时使用,是这个仓库最重要的一致性手段——见第 5 节第 5 条。
4. 主线走一遍(高层,不进代码)
一句 prompt 从进来到闭环,经过七站。怎么读: 从上往下,每一站右侧标了在哪一章讲透。
① 入口 仪表盘 / CLI / Slack / cron / webhook
│ 统一落到 projectsApp
▼
② 生命周期 排队 → 背压检查 → 建 session 记录 → 建分支 ── 第 02 章
│
▼
③ 沙箱供给 算镜像内容哈希 → 命中缓存或构建 → 开沙箱 ── 第 03 章
│
▼
④ agent 干活 守护进程克隆仓库 → 拉起 OpenCode → 执行提示词 ── 第 03 章
├── 要调外部 API?→ 走连接器网关 ── 第 05 章
└── 要调模型? → 走 LLM 网关(顺手计费) ── 第 06 章
│
▼
⑤ 提交推送 commit → 经 /v1/git 代理 push 到会话分支 ── 第 04 章
│
▼
⑥ 人审闸门 开 change request → 你 review → merge 进 main ── 第 04 章
│
▼
⑦ 配置重读 下一次读 manifest 时从 main 拿到新声明,回到 ① ── 第 01 章
四个必须说清的交接点
① → ②:入口再多,收口只有一处。 无论是 UI 点一下、kortix sessions new、Slack 里 @ 一句,还是 cron 到点,最终都变成同一批生命周期命令:createSession(apps/api/src/projects/session-lifecycle/engine.ts:96)与 startSession(同文件 :307)。入口种类被建模成一个联合类型 SessionInvocationSource(apps/api/src/projects/session-lifecycle/types.ts:6-22),覆盖 ui/cli/slack/trigger:cron 等 17 种。
③:镜像是「算」出来的,不是「记」下来的。 供给一台沙箱前先算一个内容哈希——Dockerfile 字节 + 构建上下文的 tree OID + 运行时指纹 + 硬件规格,四段拼起来做 SHA-256(apps/api/src/snapshots/hash.ts:71 computeSnapshotHash)。相同输入 → 相同哈希 → 直接命中缓存,跳过重建。四项里哪一项今天真正在起作用,见 5.1。
⑤:沙箱不知道 GitHub 的密码。 沙箱、CLI、你本机的 git,统统 clone/push 到 https://<KORTIX_URL>/v1/git/<projectId>.git,拿的是 Kortix token;API 认完 token 才用服务端铸造的短期宿主凭据把 git 协议流转给真实上游(apps/api/src/git-proxy/index.ts:1-19)。
⑦:闭环靠「重新读一遍」而不是「同步一份」。 平台不维护配置的第二副本——需要触发器/连接器/agent 声明时,现场从默认分支读 manifest(优先 kortix.yaml,回退 kortix.toml)再解析(apps/api/src/projects/triggers.ts:352 readManifest)。所以 CR 一合并,新配置自然生效。