@unieai/uad-subprocess-ssh
v0.1.21
Published
The subprocess seam placed on a machine reached over SSH
Readme
@unieai/uad-subprocess-ssh
English | 中文
subprocess 接縫,放在一台透過 SSH 連到的機器上。掛載時取代 subprocess-local,於是 harness 執行的每一個指令——Bash 工具的執行器、language server、搜尋——都移到那台機器上,而它們全都不知道這件事。
是改寫,不是重寫
這個 provider 繼承本機的那個,把每一份 spec 轉成同一個指令的 ssh 呼叫。改寫底下的一切都是本機 provider 的,已經寫好也測好:分離的行程樹、帶位移讀取器與溢出檔的 collect 模式、SIGTERM–緩衝–SIGKILL 的升級、釋放、以及行程結束時的收尾。在這裡,那套機制管的是代表遠端指令的那個 ssh 客戶端行程。
有兩件事在本機的意義必須重新指向:
- 工作目錄是遠端機器上的。客戶端本身跑在 harness 使用者的家目錄,因為 spec 的
cwd指的路徑通常不會同時存在於兩台機器上。 - 環境變數屬於遠端指令,所以它們寫在命令列裡送過去,而不是給客戶端。客戶端自己需要的——
HOME、PATH、agent 驗證用的SSH_AUTH_SOCK——來自本機 provider 自己那份已清洗的父環境。
遠端行程的生命週期
遠端指令活得比啟動它的連線久。 對真實伺服器實測:殺掉本機客戶端之後,遠端指令仍在執行——有沒有配置終端機都一樣;而一個忽略 SIGHUP 的指令,兩種情況下都活著。
所以連線不是行程的把手,pid 檔才是。遠端 shell 先記下自己的 pid 再 exec——pid 仍然是指令自己的——而終止一次執行,是開第二條(重用連線,所以很便宜)連線去對那個 pid 的行程群組送訊號。群組正是它成為整棵樹終止的原因:在 sshd 底下,指令 shell 本身就是群組的 leader,所以一個訊號就能打到指令與它啟動的一切。沒有群組可打時,退回單一 pid。
終止會做兩邊,而且兩邊都需要:結束客戶端會釋放呼叫端的串流並讓結果落地,而遠端的訊號才是真正停下工作的那一半。
客戶端結束代表什麼,取決於那是誰的結束。 遠端指令自己產生的狀態碼——255 以外的任何值——代表指令跑完了,它的 pid 檔只是殘留物,該清掉。而 255、或是被訊號終止,是客戶端結束,這對遠端行程什麼也沒說:活得比自己的連線久,正是遠端行程會做的事。這時候刪掉 pid 檔,等於把「仍在執行中的工作」的唯一把手丟掉,所以連線遺失會去終止那次遠端執行,而不是把它忘掉。
控制指令自己的離開狀態也基於同樣理由要檢查。遠端腳本一律以 exit 0 結尾,所以其他任何值都來自客戶端——被別處關掉的重用主連線、被拒絕的連線——把那當成「做完了」,會讓那台機器繼續跑著沒有人在看的工作。每一個終止步驟因此會重試一次。
終端機
終端機是一個跑著 ssh -tt 的本機 PTY,由它承載位元組、視窗大小與整個 session 的生命週期。而終端機也正是 -tt 在這裡站得住腳的原因:那些會弄壞「被收集的指令輸出」的換行轉換與串流合併,恰好是終端機想要的。
所有關於這個 session 的問題則改在那台機器上回答,因為本機 PTY 的前景行程永遠是 ssh:
- 前景是什麼,來自遠端終端機自己的
tpgid,所以它會跟著每一次fg、每一條管線、每一層巢狀程式走,harness 一件都不用追蹤。session 的 wrapper 在變成 shell 之前先記下自己的終端機(tty),第二條連線才知道要問誰。 - 訊號送到遠端的前景群組。 對本機 PTY 送訊號會打到
ssh客戶端,在使用者只想中斷一個指令的時候把整個 session 結束掉。 - 結束 session 會對遠端整個 SESSION 的每一個成員送訊號,不只是 shell 的行程群組。job control 正是終端機存在的意義:
sleep 90 &跑在自己的群組裡,只針對群組的終止會留下它繼續執行,而已經沒有人在看它了。 - shell 是不是在等輸入,讀的是前景群組的核心等待點(
wchan)。本機 provider 用的那個更強的證據——被阻擋的 syscall 與它的檔案描述子——在這裡拿不到:/proc/<pid>/syscall需要 ptrace 等級的權限,而預設kernel.yama.ptrace_scope=1的機器會拒絕第二條 SSH session 的讀取。等待點沒有名字或認不得時,這裡回報「不知道」,而不是用猜的。
它拒絕什麼
相對的執行檔路徑,與本機 provider 完全一致:它要解析的基準點並不存在,而跨過一個網路並不會生出一個。
Model Experience
間接的,透過建立在 ctx.subprocess 之上的工具——首先是 Bash 執行器——由它們擁有所有面向模型的契約;此套件自己不註冊任何 tool、prompt 或 schema,只改變它們的工作發生在哪一台機器上。
KV Cache effect
無。這裡不產生任何 prompt 片段、工具定義或 context 條目。
已知限制與未完成項目
- 「正在等輸入」是證據,不是證明。 這裡認得的核心等待點(
n_tty_read、read_chan、wait_woken、ttyin)涵蓋目前的 Linux 與 BSD;拼法不同的核心會回報「不知道」,消費端的就緒判斷就退回以沉默為依據。特別是wait_woken是個通用等待點,所以一個因為別的原因阻擋住的前景群組也可能被讀成「在等輸入」。 - 前景檢視要付一次往返,而就緒輪詢會反覆問。連線重用讓每一次都很便宜,但線路慢的機器回答輪詢就是比本機讀
/proc慢。 - 遠端必須是 POSIX 類的登入 shell,這一點記在機器名冊。
- 一次掛載一台機器。 alias 是設定,所以這個 provider 的每一次掛載,指令都跑在同一台機器上。同時要好幾台,是呼叫端的組裝——
execution-router為每台機器建一個 runtime 再從中挑一個——而那個呼叫端是直接建構這個類別的,這也是機器名冊除了注入之外還能從建構子交進來的原因:用new建出來的 provider 坐在一個什麼都沒宣告的 context 上,在那裡讀ctx.ssh會被拒絕,而不是被回答。 exitCode255 是有歧義的。 它既是 OpenSSH 自己的失敗碼,也是被終止的執行會回報的值,所以呼叫端無法只憑狀態碼區分「連線失敗」與「遠端指令被殺掉」。
