前回の記事(自作AI基盤を、AI自身に診断させた話 ― 観測を作って露見した11日間の無音バグと、LLMの所見を黙らせる決定論の檻)でCoordinator側の話は一区切りついた。今回はその直後に手をつけた、もう一つの積み残し――「サーバ上にしか実体がない設定ファイルたちを、GitLabにきちんと集約する」という地味だが避けて通れない作業の話。そして最後に、作業の記録そのものが肥大化していた話にもつながる。
サーバの中にしか実体がないファイルたち
ai-core・mars・rag-tools・macbookair2020・rtx3070ti・openwebui――6台のノードにまたがるこの基盤は、これまでソースや設定の正本が各サーバの実ファイルシステムの中にしかなかった。バージョン管理もバックアップも、せいぜい引き継ぎ資料に全文貼り付けておくという原始的な手段に頼っていた。これを一方向(サーバ→GitLab)で自動同期する仕組みを作ることにした。方針は「plan-once(設計を先に固める)→ execute-fixed(機械的な収集・md5照合・git操作だけを行う、LLMに委ねない)」。
設計書を書いた翌日、その設計書自体が事故の現場になった
設計フェーズで秘密情報の混入をチェックするルール(ファイル名denylist)を決め、いざ監査をかけてみたところ、対象は0件だった。ところが、その監査の最中に見落としていたものがあった。直前に格納したばかりの引き継ぎ資料そのものに、PostgreSQLの実接続パスワードが平文で20箇所以上埋め込まれていた。systemdのEnvironment行や操作ログの中に、うっかりそのまま残っていたのだ。しかもよく見ると、パスワードの中身はユーザー名とまったく同じ文字列だった。
幸い、まだ一度もpushしていない段階だった。git commit --amendでコミット履歴から丸ごとredactしてからpushし、GitLabには生のパスワードは一度も出ていない。とはいえ、これから作る同期システムの「秘密情報を漏らさない」設計そのものが、初日に自分の書いた資料で試されることになった。当然、該当パスワードは本人確認のうえ全ノードでローテーションした。ついでに調べると、4箇所はそもそもパスワードが未設定のまま、コード側のデフォルト値にフォールバックして認証が通っていなかった――つまり気づかないうちに何日も接続エラーを起こし続けていた。「動いているように見える」の裏に、ずっと壊れていた部分があった。
実装前に現地調査したら、設計書と実機がすでにズレていた
設計がまとまった翌日、実装に入る前にまず6ノード全部にSSHでログインして実パスを確認した。想定と違う点がいくつも出てきた。mars上のCoordinatorの実体は、設計書が想定していた~/coordinator/ではなく、電源断対策で移設済みの/opt/coordinator/だった。openwebui(Windows Server)はそもそも設計時の対象ノード一覧に入っていなかった。「書いたつもりの設計」と「今の実機」の間には、たった1日でもズレが生まれる。
もう一つ大きな穴があった。中央pull方式(ai-core自身が各ノードを取りに行く)を選んだのに、ai-coreから他ノードへのSSH鍵認証はこれまで一度も構築されていなかった。CLAUDE.mdに「SSH到達性整備済み」と書いていたのは、この作業をしているWindows機からの到達性であって、まったく別物だった。ai-coreの鍵を5箇所のauthorized_keysに追加し、rtx3070ti側はWSLにopenssh-serverすら入っていなかったのでその場でインストールし、ポートフォワード設定を22番にも拡張した。
コンテンツスキャンを足したら、また本物の不具合が出てきた
初日の事故を教訓に、ファイル名だけでなくファイルの中身から秘密情報を検出するスキャナーをgit_sync.pyに追加した。PASSWORDやAPI_KEYのようなkey=value形式とPEM鍵ブロックを検出しつつ、プレースホルダ値や環境変数参照は除外する――全121ファイルで誤検知ゼロになるまで調整した。
このスキャナーは実際に本番の問題を2件掘り当てた。ひとつはai-coreの8ファイルに残っていた、実際の接続パスワードとは一致しない無効なハードコード値。もうひとつはmarsのSearXNG設定ファイルに入っていた実際のsecret_key。当初は「環境変数で上書きされる設計のはず」と考えていたが、entrypoint.shを直接確認するとそんな上書き機構は実装されていなかった。ファイルが存在する限りテンプレート復元処理自体がスキップされる仕様で、環境変数の配線は最初から無意味だったと分かった。結局このファイルはsync_nodes.jsonのノード単位exclude機能を新設して永続的に同期対象外にした。
初回本番実行、そしてOpenWebUIだけが一筋縄でいかなかった
ai-coreに永続clone(~/mapleharp-ai-lab)を作り、5ノードで初めての本番実行。ここでもスキャナーがmacbookair2020の3つのplistとrag-tools/feed_memory_nightly.shに実運用の認証情報が埋め込まれているのを検出し、除外リストに追加。ほかにも同じ「無効なフォールバック値」パターンが6ファイルで見つかり修正した。git identityの未設定にもここで気づいて設定し、5ノード・107ファイル検証・実差分3件をcommit・push(3bfd2d3)。
最後まで残ったopenwebuiだけは事情が違った。稼働中のcoordinator_pipe.pyはディスク上にファイルとして存在せず、OpenWebUI管理画面のFunctions機能からエクスポートした時だけ手に入る、という構造になっていた。エクスポートされるJSONは複数のFunction定義の配列で、目的のPythonソースは1要素のcontentフィールドに文字列として入っている。sync_nodes.jsonにfunction_exportsという新しい設定区分を作り、最新のエクスポートJSONを自動検出してcontentを抽出し書き戻す処理を追加した。副産物として、Windowsのcmd.exeが日本語ロケール(cp932)で返す出力をPythonが厳密UTF-8デコードしようとして落ちるバグも見つかって直した。これで全6ノード・109ファイルの検証が通り、差分1件をcommit・push(a6bac54)。設計から3日ほどで、6台すべてが同じ経路で同期される状態になった。
道具ができた後、今度は記録そのものが重くなっていた
一区切りついたところで、次のセッションのために引き継ぎ資料を開こうとしたら妙に重い。行数を数えると18,794行、1.36MB。見出し構造だけを洗い出してみると、原因はすぐに分かった。ファイル末尾の「ソースコード」節、実に15,700行・約76万バイト(全体の56%)が、各ノードのソースコードを全文貼り付けた付録だった。
これは書いた当時は理にかなっていた。まだ同期の仕組みがなく、サーバの実体を保存しておく手段が引き継ぎ資料の付録しかなかったからだ。しかし今回作った仕組みによって、実体はai-core/やmars/といったノード別ディレクトリにこのリポジトリ自身の正本として存在し、変更履歴もgitで追える。付録はそのまま丸ごと重複だった。削除して1.36MBを約60万バイトへ。
削ってみるとさらにもう一段、性質の違う情報が同じファイルに同居しているのに気づいた。「設計思想」「PostgreSQLスキーマ」「デプロイ手順」のような日付に依存しないアーキテクチャの説明と、「2026-06-13セッション」のような時系列の作業ログが、ずっと交互に並んでいた。前者を独立した設計書に切り出し、引き継ぎ資料側には元の場所に橋渡しのメモだけを残した。結果、ファイルは「時系列の作業ログ」と「現行アーキテクチャの設計書」という役割にきれいに分かれた。
コードだけでなく、ドキュメントも同じ理由で膨張する。正本がどこか一箇所に定まらないまま、書いた時点の必要性だけで積み増されていく。今回作った同期システムが「サーバのどこに正本があるか」を機械的に固定する仕組みだったとすれば、最後に手を動かした引き継ぎ資料の整理は、「自分の書いたメモの正本は今どこにあるか」を人間の側が定期的に問い直す作業だった。