300ファイルを破損させた検索置換
エージェント駆動のコードベースは速く動く——そして人間のレビュアーなら一目で気づいたはずの方法で失敗する。ここに二つの失敗を公開し、それらを今きっぱりと止めるガードレールを記す。
トラブルを探しに行った日は、たいてい何かを見つける日だ。今週の土曜日、私はコードベースに対する深い監査パスをスケジュールした。計画は技術的負債に追いつくことだった:むき出しのexceptブロック、ハードコードされた定数、慣例のドリフト、高速で走るあらゆるプロジェクトに積み重なる類のもの。
私は気に入らないものを二つ見つけた。両方とも今は修正済みだ。両方とも公開する価値のある種類の発見だ。なぜなら、それらはエージェント駆動のコードベースがどう失敗するか、そして監査という規律がその失敗をどう捕まえるかについて、何か本物のことを説明しているからだ。
発見その一:コードベースを食い荒らした検索置換
プロジェクトの以前の段階で、あるエージェントがdocstringとコメント全体にわたるマーケティング風の一斉検査を依頼された。意図は妥当だった:いくつかの公開向け文字列に現れる特定のフレーズを改名すること。実行は単語境界について十分に注意深くなかった。
エージェントが入れ替えるはずだったフレーズは、「process(処理)」「success(成功)」「access(アクセス)」という単語をより長いフレーズの一部として含む特定のマーケティングタグラインだった。検索置換操作は、それらの部分文字列を一致すべきでない場所で一致させた。変数名。関数名。テストの説明文。インラインコメント。それら三つの部分文字列が現れる場所ならどこでも、エージェントの置換文字列が代入された。
結果は、識別子とプロースが微妙に歪んだ三百ファイルだった。process_eventという名前の変数は、トークンの中ほどに「Raku Game Engine Milestone」が埋め込まれた何かになった。関数の説明はナンセンスに読めた。テストの説明は、存在しないものをテストしていると主張していた。壊れた識別子はファイル内で一貫していたためコードベースはコンパイルできたが、コードベースの人間可読レイヤーは全体にわたって微妙な場所で破壊されていた。
この種の失敗がどう起きるか具体的に述べたい。これは他のチームも遭遇するであろうエージェント駆動の失敗のクラスだからだ。
検索のスコープが広すぎた。 エージェントはフレーズを見つけて置換するよう指示されていた。そのフレーズはたまたま一般的な英単語の部分文字列だった。この検索を正しくスコープする方法は、単語境界(正規表現での\bword\b)で、明示的な大文字小文字の区別で、ファイル拡張子の明示的な許可リストで、識別子コンテキストの明示的な拒否リストで行うことだ。エージェントが受け取った指示には、これらの制約が一切なかった。
エージェントは広さにフラグを立てなかった。 三百ファイルは多い。小さなマーケティングの微調整のために三百ファイルにまたがるPRを着地させたエージェントは、PRを開いた時点でその広さにフラグを立てるべきだった。エージェントはそうしなかった。PRのタイトルは「docstring内のマーケティングコピーを更新」といった程度のものだった。PR本文はファイル数を懸念としてではなく単なる数字として記載していた。
私のレビュー過程がそれを捕まえなかった。 PRの差分は、すべて同じ編集に見える三百ファイルの小さな二行変更だった。差分はざっと見ると、きれいな一斉検査のように読める。破損は、エージェントの置換がナンセンスを生み出した時点でファイルの実際の変更内容を読んだ場合にのみ表面化する。私はそれをしなかった。マージした。
CIはそれを捕まえなかった。なぜなら名前は依然としてパースできたからだ。 破損した識別子は構文的には有効だった。コンパイラは変数がマーケティングタグラインのように見える名前かどうかを気にしない。ビルドはグリーンだった。テストは実行され続けた。被害はコードの機械レイヤーではなく、人間レイヤーにあった。
今週の土曜日にどう修正したか
一つのスクリプト。スクリプトは三つのことをする。
一つ目:正規の識別子名を再導出する。 悪い検索置換が着地する前のgit履歴から、スクリプトは各識別子が本来どう呼ばれるべきだったかを再構築する。再構築は機械的だ:悪いPRが触れた各ファイルについて、PR前バージョンとPR後バージョンを差分し、置換された各トークンについて、PR前の名前への復元を提案する。ほとんどのファイルはきれいに復元される。少数は、破損の上に正当な変更が重なっていたため手動レビューが必要だった。
二つ目:grep駆動の健全性パス。 復元後でも、破損した識別子の一部は、悪いPRが着地した後に書かれた新しいコードから参照されていた。それらの参照は破損した名前に対して書かれていた。grepパスは、悪いPRが着地した後に書かれたコード内で破損したスタイルの識別子への参照をすべて見つけ、それぞれを手動判断のためにフラグする:この新しいコードは破損した名前を使う意図だったのか(まれ)、それとも単に当時存在していた名前を何でも使っていただけなのか(ほとんどのケース)?
三つ目:将来のためのガード。 エージェントが行うすべての検索置換操作は今、(a)単語境界のスコープ、(b)大文字小文字の区別、(c)ファイル拡張子の許可リスト、(d)それを超えるとエージェントがフラグを立て明示的なレビューを要求しなければならない最大ファイル数の閾値、(e)完全な置換を適用する前にエージェントが示さなければならない三つのランダムな一致のサンプル、を指定しなければならない。このガードはCopilot Guideに入っており、今やすべての検索置換タスクの枠組みの一部だ。
破損は今修復されている。修復を行った監査スクリプトはリポジトリにあり、いつでも実行可能で、証拠として差分の出力も保存されている。教訓はCopilot Guideに入っている。
発見その二:ハードコードされたHMACシークレット
深い監査パスは、もっと早く捕まえるべきだった何かも掘り起こした。ランタイムのライセンシングレイヤーは、ライセンストークンを検証するためにHMAC-SHA-256を使う。HMACシークレットはソースファイルにハードコードされていた。ソースファイルは公開リポジトリにあった。シークレットは本番検証パスで使われている実際のシークレットだった。
これは今日の中で最も気恥ずかしい発見だ。正直に述べたい。なぜならこれは、高速に動くエージェント駆動のコードベースで実際に起きる類のことであり、それを捕まえる方法についての公開議論は、非公開の議論よりもずっと価値があるからだ。
それが着地するまでにたどった経路: ライセンシングレイヤーの初期バージョンは、レイヤーが誰かに出荷される前に置き換えられる予定のプレースホルダーシークレット値でプロトタイプされた。プロトタイプは、いかにもプレースホルダーらしいdev用の値とともにPRとして着地した。時間が経つにつれ、プレースホルダーの上に実際の検証ロジックが追加された。実際に見える検証コードで包まれた瞬間、プレースホルダーはプレースホルダーらしく見えなくなった。誰かが気づいたときには、シークレットは本番スタイルのフローで使われており、ファイルは公開リポジトリにあった。
今日行ったこと:
- シークレットをローテーションした。侵害された値はもはや本番の値ではない。新しい値は環境変数にあり、実際のシークレットなしでdev作業を進められるが大声で警告する
warnings.warn()のフォールバックが付いている。 - ソースファイルからハードコードされた値を削除した。置き換えは
getenvで、本番ビルドで環境変数が設定されていない場合に明確なエラーメッセージを出す。 - 一般的なパターン(高エントロピーな文字列、base64形式のトークン、鍵らしきもの)に一致するハードコードされたシークレットをスキャンするCIチェックを追加した。このチェックは、次の試みが着地する前に捕まえる類の小さなインフラだ。
- コードベースの残りを同様のパターンについて監査するフォローアップを提出した。この監査は別の週末の作業だ。今日は目下の発見を閉じることについてだった。
ライセンシングレイヤーはまだ機能している。新しい経路はより安全だ。侵害されたシークレットは発見から数時間以内にローテーションされた。
これが何に一般化するか
いくつかの正直な指摘。
エージェント駆動の検索置換には明示的なスコープ規則が必要だ。 これは、プロジェクトの歴史の中で過度に広い一斉検査が噛みついてきた三回目だ。最初の二回はそれほど大きな被害ではなかった。今回は恒久的なガードレールに値するほど悪かった。ガードレールは今設置されている。
ソースファイル内のハードコードされたシークレットは、ツールの失敗ではなく規律の失敗だ。 実際のシークレットが公開ファイルに着地するのを許すチームを、どんなツールも救えない。「すべてのコミットはハードコードされた認証情報についてレビューされる」という規律こそが本当の修正だ。CIスキャンは助けになる。規律こそが重要なものだ。
監査はレビューが見逃したものを見つける。 PRごとのレビューが見逃しがちな失敗モードを特に探して、コードベースに対してスケジュールされた監査パスを実行する規律には、時間をかける価値がある。今日の監査は、PRレビューが素通りさせた二つのものを捕まえた。将来の監査は他のものを捕まえるだろう。頻度こそが要点だ。
パートナーとビルダーがここから学ぶべきこと
パートナーシップのためにエンジンを評価しているなら、「エージェント駆動の過度に広い一斉検査」という失敗モードをチームがどう扱っているか尋ねてほしい。正しい答えは、明示的なスコープ規則、大規模変更への強制的なフラグ立て、監査パスを含む。間違った答えは「その問題は見たことがない」だ。
自分自身でエージェント駆動のワークフローを実行していて、最近ハードコードされたシークレットの監査を行っていないなら、行ってほしい。何かが紛れ込んでいる確率はゼロではない。今それを見つけるコストは小さい。
これを読んでいるセキュリティの専門家で提案があるなら、私は本当に興味がある。私が防御しようとしている失敗のクラスは「人間のレビュアーなら一目で気づいたはずなのに、エージェント駆動のワークフローが助長する一括レビューのパターンでは捕まえられなかったことをエージェントがする」というものだ。提案を歓迎する。
土曜の午後。コードベースは厳しい検査を受けた。二つの発見、両方とも修正済み。次の監査はカレンダーに載っている。
構築に戻る。
監査されるために作られたランタイム
RakuAIは、LLMメーカーとスマートグラスメーカーが共に構築する空間ランタイムだ——監査によって規律づけられ、公開された教訓によって強化されている。パートナー級の信頼のためにどう設計しているか見てほしい。