OSIIX Library

読み取り専用という境界線

セキュリティ監視基盤をAI支援で試行錯誤した記録

Type
Development record
Published
2026-08-11
Updated
2026-08-11
Author
mars70
Overview

1. はじめに

私は個人開発でAIコーディングエージェント(ChatGPT/Codex/Claude)を使っていて、ある時点から「機能は増えているのに、なぜか信頼できなくなっていく」感覚に襲われることがありました。

私の場合、原因の多くは、AI自体の性能というより、AIが「親切心」で依頼していない範囲まで手を伸ばしてしまうことにありました。頼んでいないリファクタリング、DBスキーマの追加、既存の判定ロジックの横に似たようなロジックがもう一つできてしまう――こうした"気の利かせすぎ"はコードレビューでも気づきにくく、後から見ると広範囲に散らばっていました。

対象は、個人運用のセキュリティ監視基盤です。生ログなどを収集・解析する側(Producer)と、その結果を画面に表示・評価する側(Consumer)に役割を分けて作りました。大まかな流れは次の通りです。

フェーズ内容
R1・R2設計方針の整理
R3〜R6監視対象の実装、外部データ連携の契約策定
R7A証拠と評価の意味論の切り分け
R7B・R7C読み取り専用UIの契約策定と本番デプロイ

本稿は、この一連の流れのうち、R1からR7Cまでを対象に、AIの越権を防ぐための設計原則とガバナンスを、実際に試行錯誤しながらまとめた記録です。プロジェクト全体の完成を主張するものではなく、あくまでこの一区切りの記録です。

Example

2. 重複を避けるために先に決めたこと

このプロジェクトには、生ログを解析して攻撃と思われるパターンを抽出する別コンポーネント(Producer側)が存在します。開発の初期段階で、ローカルに残っていた別系統の設計コーパスが「重複する実装パスとして進行しうる」状態にあることが判明しました。

ここで取った対応は、コードを書く前に役割を固定することでした。「このコーパスは実装ではなく、あくまで要件定義の入力として扱う。競合する独立実装としては進めない」という方針を、ドキュメントとして先に固定しました。

地味な判断に見えますが、この判断はその後の設計とも整合していました。AIに何かを作らせる前に、やってよい範囲を人間が先に決めておく――それを徹底しました。

Design

3. 契約駆動(Contract-Driven)という順序

これまでの私の開発では、画面を作りながらデータモデルを決めることが多くありました。このプロジェクトでは意識して逆にし、次の順で決めました。

Contract(何が真実として扱われるか)
  → Meaning(各データの意味論)
  → Data boundary(誰が何を書けるか)
  → Implementation
  → UI

UIには真実を決める役割を持たせません。「画面上で正常に見えるからPASSにする」のではなく、PASSと表示してよい条件を先に契約として決めておき、それが満たされるまではUI側で判断しないようにしています。

Guardrails

4. ガードレールとして置いた制約

R7CのUI実装では、次のような制約を置きました。

  • スキーマは変更しない
  • DBは読み取り専用接続でのみ開く
  • 相関結果(Correlation)はDBに永続化せず、リクエストのたびに再計算する
  • 表示のためにモックデータで穴埋めしない

実際にR7CのUI実装コードを確認すると、DB接続部分は一貫して読み取り専用URIで開かれていて、書き込みに相当する処理は一箇所も見当たりません。相関結果の計算関数もリクエスト処理の中で都度呼び出されるだけで、DBへの書き戻しは行われていません(なお、収集側であるR3/R5/R6のcollectorスクリプトは当然DBへ書き込みを行います。read-onlyなのはR7CのUI層に限った話です)。

これらの制約は、単なる慎重論として置いたわけではありません。「UI側の要求でProducer側のデータモデルまで安易に変えない」ための壁として機能している、というのが実際のところです。この構成では、UIの都合でスキーマを1つ足すと、プロデューサー側の変更・インポーターの変更・マイグレーション・デプロイの結合にまで波及しかねません。読み取り専用の消費者に留めておくことで、少なくともUIから直接Producer側のデータを書き換える経路は持たせずに済んでいます。

Separation

5. Producer / Consumerで「意味の所有権」を分離する

Producerが「artifactが利用可能(AVAILABLE)」と報告したとしても、Consumer側でそれを「システムは健全(Healthy)」と読み替えないようにする、というルールを決めました。

Producer: 何を観測したか
Consumer: その証拠をどう表示・関連付けるか
Health評価: どの契約を満たしたときHealthと呼べるか

この3つを混ぜると、「AVAILABLE」が「HEALTHY」だと誤解されやすくなります。しかし「証拠を扱える」ことと「対象が健全である」ことは、本来別の主張です。この区別を保っておくことが、次に述べるUNKNOWNの扱いにもつながります。

State model

6. UNKNOWNを「弱いPASS」にしない

このプロジェクトで避けたかった変換は次のものです。

証拠がない → 異常の証拠がない → たぶん大丈夫 → PASS

このプロジェクトではこの変換をしない設計にしました。Healthの状態はPASS/WARN/FAIL/UNKNOWNの4状態で扱い、「未観測」「証拠不足」の場合はUNKNOWNとして、PASSにもFAILにも変換しません(なお、Correlation側の「評価対象となる相関先が存在しない」は、Healthとは別の語彙であるNOT_APPLICABLEで表現しており、Health側のUNKNOWNとは意図的に混ぜていません)。

「FAILしていない」ことと「正常であることを確認した」ことは、本来まったく違う主張です。この区別を状態として持たせるのは、エラーハンドリングというよりドメインモデルの一部だと考えています。

Maturity

7. 「動いているように見える」と「運用が成熟している」を分ける

一部の監視対象は実データを扱っていましたが、同時に「手動実行のみ」「スケジューラなし」「稼働保証なし」という制約も明記しました。実データが流れていても、それだけで本番監視が完成しているとは言えません。

手動実行のみの監視は、動かしている間は正常に見えます。しかし、次に誰かが実行するまでの間に何が起きても、それに気づく仕組みは別途用意しない限り存在しません。「見えている状態」と「継続的に確認されている状態」は、似ているようで前提が違います。

同様に、別の監視対象では「合成データでの疎通検証は完了しているが、実データでの検証は未実施」という区別も残しました。

Verification

8. 検証状況

コミット履歴上、この記録の範囲はR1クローズの2026年8月8日19:21から、R7Cクローズの8月9日14:44までです。フェーズごとの明示的なクローズアウトを伴って進行した記録として残っています。R8(以降の展開)はこの記録の対象外であり、まだ着手していません。

本稿の執筆時点で、対象範囲のテストは124件を実行し、122件が通過、2件がスキップ、失敗は0件でした(スキップの2件は、ローカル検証環境にPython 3.9が入っていないための既知の制約によるもので、実行環境に依存します)。

コミット履歴を数えると、この範囲は25コミットで構成されており、そのうち3件がfix:コミットです。ローカルのreflogを確認した範囲では、git resetや取り消しコミットの記録は見当たりません。ガードレールのおかげで手戻りがゼロだった、と主張するつもりはありません――実際に3件の修正は発生しています。ただし、少なくともGit履歴上、作業単位全体を取り消すような変更は見当たりません。

Summary

9. まとめ

ここまでの内容は、実のところCQRSで見られる更新系・参照系の責務分離や、証拠と評価を分けて考える監視設計の一般的な発想と重なる部分があります(ただし、状態をイベント列の再生から復元するような、狭い意味でのイベントソーシングそのものではありません――このプロジェクトはsnapshot/observationを直接保持しており、イベントログからの再構成はしていません)。今回意識してきたのは、「知っている概念」を、AIに実装を任せる個人開発の中でも最後まで崩さずに使えるか、という点でした。

得られた原則を4つに絞るなら、

  1. 証拠がないことを、健全である証拠として扱わない
  2. Consumerは証拠を解釈してよいが、Producerが観測した内容そのものは書き換えない
  3. 派生した結論は、できるだけ元の証拠から再計算できる状態を保つ
  4. アーキテクチャ上のガードレールは、禁止事項の列挙というより、誰が何に責任を持つかを決めるものとして使う

AIに実装を任せる場面で効いたのは、AIへの指示の出し方そのものより、AIにどこまで任せ、どこから人間が判断するかという線引きでした。地味ですが、実務上効いた設計判断です。