WebShell
WebShell はブラウザ向けのターミナルで、プラットフォームの他の部分が使う のと同じ geminio トンネル経由ですべての登録 edge に届きます。別個の SSH 踏み台もジャンプボックスもインバウンドポートもありません。edge はダイヤル アウトを続け、manager がシェル I/O 用の多重化ストリームクラスを開きます。
ユースケース:
- エージェントが修正を提案 → 「edge-prod-04 でシェルを開く」をクリック → SPA を離れずに変更を確認。
- ベンダー / 契約者が VPN 登録なしに 1 つのホストを一回限り見る必要が ある。
- インシデント対応:すべてのコマンドが起源セッションの監査行と共に記録 される。
アーキテクチャ
browser ──WebSocket──> manager:/v1/webshell/ws
│
├─ Router.Register(sessionID, sink, ActiveSession)
│
└─ geminio Stream (shell class)
│
▼
edge agent
└─ pty.Start("/bin/bash")manager 側ルーターは internal/manager/biz/webshell/router.go にあります。sessionID → Sink ディレクトリを維持:WebSocket ハンドラー が接続時に登録し、トンネル受信 dispatcher が edge の出力 / 終了プッシュを 正しいブラウザにルーティングします。
// internal/manager/biz/webshell/router.go:57
type Router struct {
mu sync.RWMutex
sinks map[string]Sink
meta map[string]*ActiveSession // sessionID → metadata
stdoutBytes sync.Map // sessionID → *uint64
}HTTP / WebSocket ハンドラーは隣の internal/manager/server/webshell に あるので、ルーターは HTTP に依存せず単体テスト可能なまま保たれます。
2 つのストリームクラス
geminio トンネルは多重化します:
- コントロールクラス —— JSON RPC(スキル実行、プラグインシグナリング、 アラート evaluator プローブ)。
- シェルクラス —— 生バイトストリーム(WebShell セッションごとに 1、
tail -fフォロワーごとに 1 など)。
トンネルレベルでの分割が重要なのは、シェル I/O がバースト的で枠なしだから です。コントロール RPC と混ぜると後者が飢餓状態になります。各クラスは 独自のバックプレッシャー予算を持ちます。
セッションメタデータ
各ライブセッションは ActiveSession を持ちます:
// router.go:37
type ActiveSession struct {
SessionID string
OngridUserID uint64
SSHUser string
DeviceID uint64
EdgeID uint64
StartedAt time.Time
LastInputAt time.Time // updated on every browser → edge frame
}LastInputAt はキーストロークごとに tick(Router.TouchInput)。 アイドルタイムアウト監視は最近の入力なしで設定された制限より古い セッションを退去させます —— 「実行中のコマンドが残ったままブラウザタブを 閉じた」リークから守ります。
監査記録
2 つのレイヤー:
- ヘッダー行 ——
webshell_sessionsテーブル:誰が、いつ、どの edge、 終了コード、合計の入出力バイト。 - ストリーム記録 —— manager 側の
Recorderインターフェイスが ワイヤーを越える全バイト(両方向、タイムスタンプ付き)を取り、/var/lib/ongrid/webshell-recordings/<session_id>.castの asciinema 互換 cast ファイルに追記します。admin の/admin/webshellページが 再生します。
Recorder インターフェイスは意図的に狭い —— 本番はファイルシンクを使用、 テストは fake を注入、将来のクラウド blob バックエンドは残りのスタックを 触らずに投入できます。
同時実行制限
ユーザーごとキャップ:WebSocket open ハンドラーから Router.CountByUser が呼ばれ、キャップ超え接続は HTTP 429 で拒否されます。デフォルトキャップ は 5(設定可能)。edge ごとキャップは暴走エージェントが 100 並列シェルを 開くのを防ぎます。
セッションの強制終了
3 つのパスがセッションを強制終了します:
- ブラウザクローズ —— WebSocket 切断が edge に伝播し、edge が pty を
kill -HUPする。 - Admin 強制終了 —— admin SPA が
SinkにKiller.Kill(reason="admin terminated")を呼び、edge にクローズを トンネルダウンする。理由はセッションの終了行に記録される。 - アイドル退去 —— 監視が
LastInputAtがキャップを超えたセッション にKill("idle timeout")を発火する。
// router.go:50
type Killer interface {
Kill(reason string)
}manager 側ハンドラーは Sink を登録するときに Killer をインストール します。opt-in した Sink は admin 強制終了可能になり、それ以外は ブラウザクローズ強制終了のみ可能です。
ロールゲーティング
WebShell は admin ロールでゲートされます(ADR-022 RBAC)。user ロールはエージェントとチャットできますがシェルは開けません。viewer は 過去のセッションの記録を読めますが新規は開けません。ゲートは WebSocket アップグレードの前、HTTP ハンドラーエントリで動きます。
関連
- スキル ——
bashスキルは WebShell のワンショット相当 (単一コマンド、pty なし)。同じ監査基盤。 - Edge インストール —— WebShell が届けるようホストの edge エージェントを立ち上げる。
- アーキテクチャ —— geminio トンネルの 位置。