内容
<p align="center"><img src="assets/wordmark.png" alt="導管" width="300"></p>
# 導管
<p align="center"><b>您的 AI 編程代理構建並運行工作流程。您不需要點擊。</b></p>
<p align="center">代理操作的自動化運行時 · 確定性腳本在 Docker 中 · <b>內部零 LLM</b> · Apache-2.0</p>
---
**導管**是一個為代理時代構建的自動化運行時。與其讓人類點擊 UI,不如讓您的編程代理建立、運行和調度工作流程,並通過 MCP 與導管通信。該平台在乾淨的容器中運行確定性代碼,**並且零令牌消耗**:昂貴的智能保留在您的代理中,在運行時外部。
可以把它想象成 *Sidekiq/cron for AI agents*:代理腳本機械化 80%,導管廉價且可靠地執行,您不需要觸摸儀表板。
## 為什麼是導管
- **代理操作。** 您的代理通過 MCP 上傳、連接、觸發、讀取日誌。沒有 UI。
- **內部零 LLM = 零令牌消耗。** 確定性代碼,而不是 LLM 在循環中。平台從不花費令牌來運行您的工單。
- **當 hermetic 時是確定性的。** 一個任務 = 一個乾淨的容器,每個任務的 CPU/內存上限、超時、重試。網絡 **默認啟用** (大多數監視器都會命中 API);設置 `network=false` 以獲得一個純函數式的 hermetic 運行,其輸出是其輸入的純函數。相同的輸入會命中一個內容地址緩存,而不是重新計算。每個運行都帶有一個防篡改收據(網絡運行的建議,因為其輸出可以取決於外部世界)。
- **真實觸發器。** cron + 入站網絡鉤(POST /hook/<token>,Stripe/Calendly/GitHub-ready)。
- **節儉令牌。** 每個 MCP 響應都針對代理的上下文預算進行調整 — ID 而非有效負載,分頁日誌,計數而非轉儲。
## 工作原理
```mermaid
flowchart LR
agent["您的編程代理"] -- MCP --> dokan["導管守護進程<br/>(Rust · axum + rmcp)"]
dokan -- 狀態 --> pg[("Postgres")]
dokan -- 一個任務,一個容器 --> c1["任務容器<br/>(隔離;默認網絡啟用,關閉 = hermetic)"]
c1 -- "stdout · ::dokan:result::" --> dokan
dokan -- "結果 POST" --> relay["您的代理 / 中繼"]
human["操作員(可选)"] -. 監視 .-> cockpit["駕駛室 /"]
cockpit --- dokan
```
一個單獨的 Rust 守護進程(axum + rmcp MCP 服務器,stdio 或 Streamable HTTP)。狀態存儲在 Postgres 中。執行是一個任務 → 一個新容器(`python:3.12-slim` / `node:22-slim` / `alpine`),運行後丟棄,帶有每個任務的上限和硬超時。日誌流式存儲到 Postgres,提供光標分頁。一個薄的操作員駕駛室運行在 `/`,Prometheus運行在 `/metrics`。**導管內不運行 LLM** — 代理是唯一花費令牌的地方。
## 快速入門
前提:Rust 工具鏈和 Docker 正在運行。守護進程與默認的 Docker 套接字通信 — 如果您運行 colima / podman / 遠程引擎,請設置 `DOCKER_HOST`。其默認 `DATABASE_URL` 已經指向 compose 數據庫,因此無需配置。
```sh
docker compose up -d # Postgres 狀態存儲(pgvector)位於 :5499
cargo build --release
./target/release/dokan # HTTP 守護進程位於 127.0.0.1:8088 — UI 位於 /,MCP 位於 /mcp
```
架構遷移在啟動時自動應用。驗證它是否啟動:打開 <http://127.0.0.1:8088/>(操作員駕駛室)— 加載後,代理可以連接 MCP 並運行腳本。
## 運行您的第一個任務
啟動守護進程後,運行旗艦演示 — 它通過 MCP 驅動一個真實的 DAG(上傳 4 個確定性步驟,連接一個具有 `map` 風扇出和 `when` 分支的流程,運行它,打印結果)。無需密碼,無需任務網絡,完全可重現。需要 `curl` + `jq`:
```sh
./examples/flagship/run.sh # 使用 DOKAN_ADDR=host:port 覆蓋守護進程
```
這是整個循環 — 上傳 → 組合 → 運行 → 讀取 — 在一個命令中。請參閱 [`examples/flagship/`](examples/flagship/) 以獲取步驟和預期的輸出。
## 連接到您的代理(MCP)
將您的代理的 MCP 配置指向守護進程:
```jsonc
"dokan": { "type": "http", "url": "http://127.0.0.1:8088/mcp" }
```
您的代理現在通過 MCP 擁有完整的導管工具集。
## 核心概念
### 腳本
腳本是 `python` | `node` | `bash` 中的代碼。它從 **`DOKAN_INPUT`** 環境變量讀取輸入(一個 JSON 字符串 — 不是 stdin,不是 argv)。密碼作為自己的環境變量到達。`upload_script(..., upsert=true)` 重新配置並命名為等幂,因此重新啟動的代理永遠不會留下孤立的副本。腳本的非零退出被視為腳本自己的**確定性裁決**(例如監視器發現)並且 *不* 重試 — 只有真正的基礎設施失敗(容器消失 / 超時)才會重試。
### 流程(DAG)
`compose_flow` 將腳本連接成一個驗證的非循環圖;`run_flow` 運行它,引擎驅動 DAG。每個步驟是一個容器運行,看到 `{flow_input, deps, step}` 作為其 `DOKAN_INPUT`。
```mermaid
flowchart LR
intake["導入"] --> score["評分<br/>(映射:每個訂單一個運行)"]
score --> summarize["總結<br/>(結構化結果 + 分支令牌)"]
summarize -- "當 deps.summarize == FLAGGED" --> alert["警報"]
summarize -- 否 --> done(["完成"])
```
引擎提供:
- **`map` 風扇出** — 每個子項運行一個;父節點將子節點折疊成 `{n, ok, failed}` 計數,而不是列出每個子項。
- **`when` 分支** — 只有當上游結果的謂詞成立時,步驟才運行。
- **`compensate`(事務回滾)** — 一個步驟可以聲明一個補償動作,如果後續步驟失敗,則該動作運行,以便部分應用流程乾淨地解開。
- **重試** 具有暫時失敗的回退。
- **步驟邊界耐久性** — 進度在每個步驟提交,因此崩潰的引擎恢復;成功的步驟被跳過。步驟必須是冪等的(一個垂死的步驟重新運行)。
### 調度和觸發器
- **`schedule(script_id, cron)`** — **6 字段 cron 帶有前導秒** (`0 */5 * * * *` = 每 5 分鐘)。5 字段表達式被大聲拒絕,而不是靜默地從不觸發。每個滴答聲都排隊一個運行。
- **`create_webhook`** — 外部 `POST /hook/<token>` 將腳本或流程與請求正文作為輸入排隊。不可猜測的 URL 令牌是身份驗證(端點位於持有者門外)。
### 密碼
`set_secret(name, value)` 一次 → 注入為每個作業容器的環境變量(例如 `$OPENAI_API_KEY`)。**僅寫入**:值永遠不會返回或日誌記錄;`list_secrets` 僅顯示名稱。
### 資源限制
每個任務的上限默認設置為 **1024 MiB / 2.0 CPU**,在守護進程上全局設置。單個較重的腳本可以覆蓋它們 — `upload_script(..., mem_limit_mb, cpu_limit)` — 只有該腳本在具有提升上限的專用容器上運行;其他所有內容保持不變。
### 結構化結果和狀態監視器
作業打印一行 `::dokan:result:: {json}` 到 stdout;導管捕獲 **最後** 一個作為運行的結構化結果。它由 `wait_for`/`read_logs` 返回,並在完成時 POST 到中繼 — 因此監視器發出發現,代理做出 **事件驅動** 的反應,無需輪詢。
對於只在某些東西更改時觸發的監視器,請設置 `upload_script(..., feed_prev_result=true)`:導管將上一個運行的結構化結果作為 `DOKAN_INPUT.prev_result` 提供給下一個運行。監視器讀取 `prev_result.state`,與當前狀態進行差異比較,發出新狀態,如果更改則退出非零 — 跨運行差異比較,無主機文件,無外部存儲,在無狀態運行時。
### 運行工件(輸入文件)
要將真實文檔交給作業 — PDF,數據集,大型 `.md` — 無需將其塞入 `DOKAN_INPUT`(環境變量,~100 KB):`upload_blob(bytes)` 將其存儲在內容地址 blob 存儲中(重新上傳相同的位元組進行重複數據刪除)並返回句柄;`run_script(..., files={"doc.md": "<handle>"})` 將每個文件 **只讀地掛載在 `/input/<name>`** 在容器中。blob 哈希折疊成運行的緩存鍵和收據,因此讀取 `/input` 的作業仍然是其聲明輸入的純函數。
### 確定性和收據
`network=false` 作業是 **其輸入的純函數** — 源 + `DOKAN_INPUT` + 輸入文件 blob + 固定的映像摘要。三個結果:
- **內容地址緩存。** `run_script(..., cache=true)` 回憶先前的相同成功,而不是重新計算 — 沒有容器啟動。
- **防篡改收據。** 每個運行都帶有一個收據綁定(映像摘要,源哈希,輸入哈希,輸出哈希,輸入 blob 哈希)下一個由 `DOKAN_RECEIPT_KEY` 密鑰的 HMAC。它檢測對於持有該密鑰的人的篡改;它 *不是* 一個公共的,第三方可驗證的簽名(需要一個非對稱方案 — 在路線圖上)。網絡作業的收據是建議性的,因為其輸出可以取決於外部世界。
## MCP 接口(節儉令牌)
| 工具 | 返回 |
|---|---|
| search_script · list_scripts · get_script | 排名 / 列出 ID + 1 行描述;僅需時返回正文 |
| upload_script | 腳本 ID + 版本(標誌:網絡,內存/CPU 限制,feed_prev_result) |
| run_script | 立即返回運行 ID,永不阻塞(輸入 + `files` 為 /input 工件) |
| read_logs · wait_for | 光標日誌 / 長輪詢到終端 + 尾部 + 結構化結果 |
| schedule · list_schedules · unschedule | cron 腳本(6 字段) |
| compose_flow · run_flow · get_flow_run | 聲明式 DAG;每步驟狀態(映射子節點折疊為計數) |
| create_webhook · list_webhooks · delete_webhook | 入站 HTTP 觸發器到腳本/流程 |
| upload_blob · download_blob · list_blobs | 內容地址輸入/輸出文件 |
| get_receipt | 運行的防篡改再現性收據 |
| set_secret · list_secrets | 僅寫入密碼,注入為作業環境 |
| on_result · list_triggers · delete_trigger | 反應觸發器:當結果匹配時排隊腳本 |
| cancel · list_runs · list_executors | … |
服務器指令在線傳送,因此代理自我限制。
## 狀態
**v0.2.x — 测试版 / 预览版。** 积极开发中,由团队自身构建和运行(我们运行自己的代理舰队的自动化流程,使用导管)。 **准备好:演示,设计合作伙伴,技术早期采用者。** 尚未完全成熟的多租户企业(无SSO/RBAC/HA,密码对所有作业都是全局的 — 参见 [SECURITY.md](SECURITY.md))。诚实地讲,它还处于什么阶段。
## 许可
Apache-2.0. 使用它,嵌入它,构建于它之上。
---
* [tsukumo](https://tsukumo.ch) 套件的一部分:用于大规模运行 AI 代理的开放工具。