前回の記事(AIDR: AI Detection & Response 構想)で、CC Pipeline の限界を3つ整理しました。
- 自己申告テレメトリでは OS 上の実挙動が見えない -- hooks で拾えるのはエージェントの「やります宣言」だけ。実際にホスト上で何が起きたかは分からない
-
hooks は「最初の一手」しか見えない --
npm installの postinstall スクリプトや、書いた Python スクリプトの内部動作は追跡できない - AI は人間の意図を察せない -- プロンプトに書かれていないことは存在しないのと同じ。意図との乖離は検知しようがない
記事の最後に「次にやるとしたら Sysmon ログと CC Pipeline の state.json を突き合わせる簡易 PoC あたりからだと思います」と書きました。やりました。ついでに他の2つの限界にも PoC を作りました。
この記事では、3つの PoC の設計判断、実装で見つかった問題、そして比較評価の結果を書きます。どれを CC Pipeline 本体に統合すべきか、という判断までやってみました。
なぜ3つ作ったか
限界が3つあるなら、対処法も3つ要ります。それぞれ異なるアプローチで独立した PoC を作り、比較することにしました。
| PoC | 対処する限界 | アプローチ |
|---|---|---|
| A: Sysmon 突き合わせ | OS 上の実挙動が見えない | Sysmon ログと state.json の相関分析 |
| B: 実行後チェック | 最初の一手しか見えない | Pre/PostToolUse のスナップショット差分 |
| C: 意図逸脱可視化 | AI は意図を察せない | プロンプトからの意図抽出 + trace との比較表示 |
実装前に捨てた選択肢もあります。
- LLM による意図解析: 外部依存なし(stdlib のみ)の CC Pipeline 設計思想に反する
- リアルタイム Sysmon 相関: 常駐プロセスが必要で、短命プロセス設計に合わない
- PostToolUse での stdout 解析: hooks には Bash の stdout と stderr が結合されたテキストとして渡されるが、コマンドごとに出力形式が異なり、汎用的な解析ルールを作るのは現実的でない
PoC A: Sysmon 突き合わせ型
解決したい問題
CC Pipeline の hooks が捉えるのは、エージェントの自己申告です。「今からこのファイルを読みます」「このコマンドを実行します」という宣言を受け取っています。でも実際に何が起きたかは、OS 側でしか分かりません。
Windows には Sysmon があります。プロセス生成、ファイル作成、ネットワーク接続をイベントログに記録してくれます。これと state.json を突き合わせれば、「エージェントが言っていることと、実際に起きていることの乖離」が見えます。
設計判断
バッチ分析にしました。リアルタイム相関は魅力的ですが、常駐プロセスが必要になります。CC Pipeline の設計思想(短命プロセス)に合いません。セッション終了後にまとめて分析する形式にしました。
wevtutil 経由で Sysmon ログを取得します。手元の環境では PowerShell の Get-WinEvent より速く感じました。XML をパースして構造化します。
タイムスタンプ相関がコアです。エージェントのツール呼び出しと、Sysmon イベントを時間窓(デフォルト3秒)で突き合わせます。候補抽出は bisect で O(n log m) に抑えています(その後の貪欲マッチングでソートが入るため、イベント密度が高い場合は伸びます)。
中核コード
correlator.py の相関ロジックです。やっていることは単純で、時間的に近いイベント同士をペアにしているだけです(実装ではツール実行時間による窓の延長やタイムスタンプ欠損の除外なども入っていますが、ここでは省略しています)。
def correlate(trace, host_events, window_sec=3.0):
# ホストイベントを時刻でソート
indexed_host = sorted(enumerate(host_events), key=lambda x: x[1].get("t", 0))
host_times = [he.get("t", 0) for _, he in indexed_host]
# 各エージェントイベントに対して、時間窓内のホストイベントを探す
candidates = []
for i, agent_entry in enumerate(trace):
agent_t = agent_entry.get("t", 0)
lo = bisect.bisect_left(host_times, agent_t - window_sec)
hi = bisect.bisect_right(host_times, agent_t + window_sec)
for k in range(lo, hi):
orig_j, he = indexed_host[k]
delta = abs(he.get("t", 0) - agent_t)
candidates.append((delta, i, orig_j))
# 時間差が小さい順に貪欲マッチング
candidates.sort()
used_host = set()
agent_matches = {}
for _delta, i, j in candidates:
if j in used_host:
continue
used_host.add(j)
agent_matches.setdefault(i, []).append(j)
出力は3種類のリストです。
- matched: エージェントイベントと対応するホストイベントのペア
- orphan_host: 対応するエージェントイベントがないホストイベント(これが怪しい)
- orphan_agent: 対応するホストイベントがないエージェントイベント
orphan_host が検知シグナルになります。エージェントが申告していないのにホスト上でプロセスが動いた、ファイルが作られた、ネットワーク通信が発生した、という状況です。
viewer.html
左にエージェントのトレース、右にホストイベントを配置しています。時間軸を揃えて並べ、マッチしたものは線で結びます。orphan_host は赤くハイライトされます。
特徴と限界
特徴:
- OS レベルの不可視イベントを発見できる
- Sysmon の豊富な情報(プロセスツリー、コマンドライン、ハッシュ)が使える
- 分析処理はセッション終了後に回すため、セッション中の追加負荷なし
限界:
- Sysmon が必要(Windows 限定)
- 3秒の時間窓はヒューリスティック。ノイジーになりやすい
- 日常利用には向かない(セッション終了後の分析)
PoC B: 実行後チェック型
解決したい問題
hooks は「最初の一手」しか見えません。npm install を実行したことは分かります。でも postinstall スクリプトが裏で何をしたかは分かりません。
なら、コマンド実行の前後でファイルシステムの状態を比較すればいいはずです。「実行前にはなかったファイルが増えている」「実行前と内容が変わっている」が分かれば、副作用の検出になります。
設計判断
Pre/Post の2 hooks 構成です。PreToolUse でスナップショットを取り、PostToolUse で差分を比較します。
スナップショットは CWD(作業ディレクトリ)直下のみです。全ファイルシステムを見るのは重すぎます。作業ディレクトリ直下のファイルリストとハッシュだけを記録します。サブディレクトリの中は見ていません。
ハッシュは高速化優先です。ファイル内容のハッシュではなく、ファイルリスト自体のハッシュです。変更があったかどうかだけ分かればいいという判断です。
中核コード
snapshot.py(PreToolUse)でスナップショットを取ります。
def collect_snapshot(cwd):
cwd_hash, cwd_count = dir_listing_hash(cwd)
cwd_files = {}
for name in sorted(os.listdir(cwd))[:MAX_DIR_ENTRIES]:
full = os.path.join(cwd, name)
st = os.stat(full)
cwd_files[name] = {"size": st.st_size, "mtime": st.st_mtime}
return {
"cwd": cwd,
"cwd_hash": cwd_hash,
"cwd_count": cwd_count,
"cwd_files": cwd_files,
}
postcheck.py(PostToolUse)で差分を検出します。核心部分を抜粋します(実際のコードには None ガードや bulk_file_change 検出も含まれています)。
def compare_snapshots(pre, post):
effects = []
if pre["cwd_hash"] != post["cwd_hash"]:
pre_files = set(pre.get("cwd_files", {}).keys())
post_files = set(post.get("cwd_files", {}).keys())
added = post_files - pre_files
removed = pre_files - post_files
modified = [name for name in pre_files & post_files
if pre["cwd_files"][name] != post["cwd_files"][name]]
if added:
effects.append({"type": "files_created", "files": sorted(added)})
if removed:
effects.append({"type": "files_removed", "files": sorted(removed)})
if modified:
effects.append({"type": "files_modified", "files": modified})
return effects
viewer_b.html
コマンドごとに副作用を表示します。ファイル追加は緑、削除は赤、変更は黄色です。severity(info/warn/danger)でフィルタできます。
特徴と限界
特徴:
- 常時稼働できる(hook として登録するだけ)
- 外部依存なし(stdlib のみ)
- 副作用の可視化は直感的
限界:
- CWD 外やサブディレクトリ内の変更は見えない
- ファイル内容の中身までは見ていない
- 実行中に人間や別プロセスが変更したファイルも、そのコマンドの副作用として記録される(区別できない)
- Pre/Post の2 hooks が必要でオーバーヘッドがある
PoC C: 意図逸脱可視化型
解決したい問題
AI は人間の意図を察せません。プロンプトに書かれていないことは、存在しないのと同じです。
AIDR 記事で書いた話です。ルール通りに最適手は打てます。でも「読み」ができません。プロンプトから明らかに逸脱した操作は検知できますが、プロンプト自体が曖昧で人間の真意と AI の解釈がズレているケースは、AI 側からは判断できません。
なら、「判断できない」ことを可視化して、人間に委ねればいいはずです。
設計判断
自動判定はしない。これが最大の設計判断です。「逸脱かどうか」を機械的に決めません。意図とトレースを並べて表示し、人間が見て判断する設計にしました。
プロンプトから意図を抽出します。正規表現でファイルパス、操作キーワード(fix/add/delete/update/test/deploy 等)、スコープヒント、否定スコープを抽出します。日本語にも対応しています。
カバレッジ率を算出します。トレース内の操作のうち、意図に合致するものの割合を計算します。100% なら全操作が意図の範囲内です。低ければ逸脱の可能性があります。
中核コード
intent_parser.py でプロンプトから意図を抽出します。
# 操作キーワード(英語 + 日本語)
_OPS = {
"fix": re.compile(r'\b(?:fix|repair|patch|debug|resolve)\b', re.I),
"add": re.compile(r'\b(?:add|create|implement|build|make)\b', re.I),
"delete": re.compile(r'\b(?:delete|remove|drop|clean)\b', re.I),
"update": re.compile(r'\b(?:update|change|modify|edit|refactor)\b', re.I),
"test": re.compile(r'\b(?:tests?|spec|assert|verify|check)\b', re.I),
# 日本語
"fix_ja": re.compile(r'(?:修正|直し|バグ修正|修復)'),
"add_ja": re.compile(r'(?:追加|作成|実装|新規)'),
...
}
def parse_intent(prompt_text):
return {
"paths_mentioned": extract_paths(prompt_text),
"operations": extract_operations(prompt_text),
"scope_hints": extract_scope_hints(prompt_text),
"negative_scope": extract_negative_scope(prompt_text),
}
deviation_checker.py でトレースと比較します。
def check_deviation(intents, trace):
for entry in trace:
detail = entry.get("detail", "")
stage = entry.get("stage", "")
# 否定スコープに触れていたら警告
if _in_negative_scope(detail, all_negative):
relevance = "negative_hit"
# インフラ系ステージは常に in_scope
elif stage in ("think", "gate", "subagent", "prompt", "session", "compact", "done"):
relevance = "in_scope"
# パスマッチまたはスコープマッチ
elif _path_matches(detail, all_paths) or _scope_matches(detail, all_scope):
relevance = "in_scope"
# ステージと操作の一致(verify + test 等)
elif _stage_matches_ops(stage, all_ops):
relevance = "likely_in_scope"
else:
relevance = "out_of_scope"
viewer_c.html
左パネルに意図(抽出されたパス、操作、スコープ)、右パネルにトレースを配置しています。カバレッジメーターが上部に表示されます。OUT OF SCOPE セクションが一目で分かるようハイライトされます。
特徴と限界
特徴:
- 常時稼働できる(UserPromptSubmit hookのみ)
- 外部依存なし(stdlib のみ)
- 人間判断に委ねる設計。「察せない AI」の限界を認めた上での解決策
限界:
- 意図抽出は正規表現ベース。複雑なプロンプトには対応しきれない
- 誤検知は低いが、見逃しも多い
- カバレッジ率の「低い」の閾値は主観的
レビューと修正
3つの PoC を作った後、並列でコードレビューを実施しました。共通の問題がいくつか見つかりました。
共通の問題
ファイルロック欠如: 並列ツール呼び出しや複数セッションで state.json への書き込みが競合します。CC Pipeline 本体では対策済みでしたが、PoC にはコピーし忘れていました。←ばか
viewer の XSS 脆弱性: クラス名やファイルパスをそのまま innerHTML に入れていました。悪意のあるファイル名があると任意のスクリプトが実行されます。←ばか2
with_lock の例外黙殺: これは PoC B と C の2本にあった問題です。ロック取得失敗時に例外を握りつぶして続行していました。修正済みです。
修正結果
| PoC | 修正前テスト数 | 修正後テスト数 | 品質スコア |
|---|---|---|---|
| A: Sysmon 突き合わせ | 25 | 33 | 4.0/5 |
| B: 実行後チェック | 24 | 27 | 3.0/5 |
| C: 意図逸脱可視化 | 41(内容更新) | 41 | 3.5/5 |
なお、次の比較評価は上記の問題をすべて修正し終える前に実施しています。スコアは評価時点の状態を反映したものです。
比較評価
10軸で評価しました。重み付けは「日常利用で価値があるか」を重視しています。
| 軸 | 重み | A: Sysmon | B: 実行後 | C: 意図逸脱 |
|---|---|---|---|---|
| 検知価値 | x3 | 5 (高) | 3 (中) | 3 (中) |
| 実装品質 | x3 | 4 (良) | 3 (評価時点で残存バグ) | 3.5 (良) |
| 統合容易さ | x3 | 2 (難) | 4 (中) | 5 (易) |
| 誤検知率 | x2 | 2 (高) | 4 (中) | 5 (低) |
| ランタイム負荷 | x2 | 5 (なし) | 3 (中) | 4 (低) |
| 外部依存 | x2 | 1 (Sysmon) | 5 (なし) | 5 (なし) |
| 日常利用適性 | x3 | 2 (低) | 4 (中) | 5 (高) |
| ユーザー体験 | x2 | 4 (事後分析) | 3 (リアルタイム) | 4 (直感的) |
| 記事映え | x1 | 5 (面白い) | 3 (地味) | 4 (コンセプト強) |
| 将来拡張性 | x1 | 4 (Elastic連携可) | 3 (限定的) | 4 (LLM連携可) |
重み付き加重スコア(5点満点 x 重み、満点110):
- A: 72/110
- B: 78/110
- C: 93.5/110
PoC C が最高スコアでした。理由は明確で、日常利用適性と統合容易さが高いです。AIDR 記事のテーマ「察せない AI」と直結しているのもポイントです。
PoC A は検知価値は最高ですが、Sysmon 依存と日常利用適性の低さが足を引っ張りました。Windows 専用で、セッション終了後にしか使えないのは厳しいです。
PoC B は中庸です。悪くないですが、評価時点では残存バグがあり、CWD 限定という制約もあります。
統合方針
段階的に統合します。
第 1 段階: PoC C を本体統合
UserPromptSubmit hook1本だけなので統合が簡単です。viewer_c.html を本体ダッシュボードに組み込みます。意図抽出とカバレッジ表示を常時有効にします。
第 2 段階: PoC B をオプトインで検討
残存バグは修正済みなので、次は実運用での負荷を見て判断します。Pre/Post の2 hooks 構成はオーバーヘッドがあるので、オプトイン機能にする可能性が高いです。
PoC A は独立ツールとして残す
本体には統合しません。Sysmon がある環境向けの分析ツールとして、別リポジトリに切り出すことを検討しています。SIEM 連携する方向性もあります。
学んだこと
「察せない」問題は解決しない
PoC C を作って改めて思いましたが、AI が意図を察せない問題は技術では解決しません。できるのは「察せないことを可視化して人間に委ねる」だけです。
プロンプトを完璧に書けば解決するかというと、人間自身が意図を完全に言語化できない以上、同じ限界にぶつかります。
バッチ分析 vs 常時稼働
PoC A(バッチ分析)と PoC C(常時稼働)を両方作って分かったことがあります。日常利用には常時稼働が圧倒的に有利です。バッチ分析は検知価値が高くても、実際に使われません。
今回の比較を見る限り、CC Pipeline の設計思想「短命プロセス」は妥当だったと感じています。
テストは品質の指標
テスト数の増減が品質を反映していました。レビューで問題が見つかると、その修正のためにテストが増えます。テストが増えないのは、問題が見つかっていないか、見つかっても放置しているかのどちらかです。
PoC B のテストが 24 から 27 にしか増えなかったのは、評価時点で残存バグの修正を後回しにしていたからです。
まとめ
CC Pipeline の3つの限界に対して、3つの PoC を作りました。
| PoC | アプローチ | スコア | 判定 |
|---|---|---|---|
| A: Sysmon 突き合わせ | OS イベントとの相関 | 72/110 | 独立ツール化 |
| B: 実行後チェック | Pre/Post スナップショット差分 | 78/110 | オプトインで検討 |
| C: 意図逸脱可視化 | プロンプトからの意図抽出 | 93.5/110 | 本体統合 |
PoC C を本体に統合します。「AI は意図を察せない」という限界を認めた上で、「察せないことを可視化して人間に委ねる」設計です。AIDR 記事で書いた思想をコードにした形になりました。
CC Pipeline は当初「可視化・検知・ブロック」まででした。PoC C の統合で「意図との乖離の可視化」が加わります。まだ「対応・封じ込め」には至っていませんが、一歩前進しました。
おわりに: 人間レビューに帰ってきたという話
改めて振り返ると、面白い構図になっています。CC Pipeline を作った出発点は、人間が全ツール呼び出しを目で追うのは無理だ、というところでした。見落とすし、承認ボタンを流れで押してしまう。だからパターンマッチで機械に止めさせる、という発想です。
3つの PoC を比較して選んだのは、最終判断を人間に委ねる PoC C でした。人間を信用しないために作った仕組みが、一周して人間レビューに帰ってきたわけです。
ただし、同じ場所に戻ったわけではありません。人間に戻ってきたのは「意図と合っているか」の判断だけです。これは AI が原理的に持てない情報で、意図を持っている本人にしか判断できません。機械にできるのは、その判断に必要な材料を、見落としにくい形で並べるところまでです。
つまり分業が変わりました。人間が苦手な「全部を漏れなく見る」は機械に渡し、人間にしかできない「意図と照らす」だけを人間に残す。信用しないのは人間の注意力で、信用するのは人間の意図です。人間に委ねる範囲を、そこまで絞り込めたのが今回の成果だと思っています。


