セキュリティ、ライブストリーミング、リプレイ、因果関係
Chronicle を「賢い」と感じさせる 4 つのサブシステム — 秘密情報のリダクション、ライブセッションの追尾(tailing)、決定論的なリプレイ、read→change の因果関係 — は、すべてローカルなヒューリスティックです。このスタックのどこにも LLM 呼び出しはなく、これこそがデフォルトでオフラインという保証を守るものです。
このページは、コントリビューター向けに server/security.js、hooks/chronicle-guard.mjs、server/live.js、server/replay.js、server/causality.js、server/shares.js を扱います。各セクションは、データフローとそれが満たす設計上の制約を説明します。共通する筋は、ここでのすべての「知的な」振る舞いが、信頼しなければならないモデルではなく、あなたが読んで監査できるパターンマッチングと構造分析であるということです。
セキュリティエンジン(server/security.js)
リダクションエンジンはテキストを { findings, redacted } に変換します。組み込みの検出器の固定セットとユーザー定義のグロブルールを組み合わせ、重複を優先度で解決し、元のデータには決して触れません — リダクションは一方向で、コピーにのみ適用されます。
組み込みルール
BUILTIN_RULES は、設定不要な正規表現検出器の順序付き配列です。
| ルール id | 検出対象 | リダクション例 |
|---|---|---|
api_key | sk-…、anthropic-…、AKIA…、ghp_…、xox…、AIza… | sk-**** |
password | password/secret = 値 | キーを残し、値をマスク |
token | Bearer …、JWT(eyJ…) | eyJ**** |
db_conn | postgres://、mysql://、mongodb://、… | **** |
email | メールアドレス | ***@***.com |
phone | 電話番号 | ***-***-**** |
private_ip | 10.*、127.*、192.168.*、172.16–31.* | ***.***.***.*** |
順序が重要です。db_conn は意図的に email/password より前に実行され、接続文字列がメール一致とパスワード一致に細切れにされるのではなく、全体として一つにリダクションされるようにします(これは「具体的なものを広範なものより先に」というルールで、配列の順序に焼き込まれています)。
カスタムルールと優先度
カスタムルールはグロブです — * は非空白文字の任意の連なりに、? は 1 文字に一致します — これらは globToRegex() によって正規表現にコンパイルされます。ルールは、redact ルール(KITE-*、*@company.com)か、あるスパンをリダクションから守る allow ルールのいずれかです。scanText() は、同じ文字を巡って競合するすべてを、固定された優先度で解決します。
- 許可リスト(allow-list)が勝つ。
allowルールに一致したスパンはまず保護され、決してリダクションされません。 - カスタム
redactルールが組み込みより先。 それらはルールセット内でBUILTIN_RULESの前に差し込まれます。 - 重複時は先の一致が勝つ。
claimed(占有済み)区間リストは、あるスパンが一度リダクションされると、後のルールが重複する文字を再占有できないことを意味します。
結果は決定論的です。findings は位置でソートされ、リダクション済みの文字列は占有済みスパンの上に置換を差し込むことで再構築されます。エクスポート: listRules、addRule、deleteRule、toggleRule、scanText(text)、scanSession(messages)、preToolUseCheck(...)、listInterceptions。
scanSession(messages) は、セキュリティチェックと共有作成で使われるバッチパスです。各メッセージの text と tool_input をスキャンし、メッセージごとの findings とリダクション済みコピーを返し、totals ヒストグラムと findingCount を集計します。
pre-tool-use パス
preToolUseCheck({ tool_name, tool_input }, readFileFn) はライブガードのエントリポイントです。読み取り系ツール(Read、read_file、View、Grep、NotebookRead)に対しては、注入された readFileFn を介して実際のファイル内容をスキャンし、それ以外については直列化されたツール入力をスキャンします。ブロックするのは高重要度ルールのみです。
const HIGH_SEVERITY = new Set(['api_key', 'password', 'token', 'db_conn']);高重要度の findings(または任意のカスタムルール)は、人間可読な理由とともに decision: 'block' を返します。低重要度の一致(email、phone、private IP)は flagged(フラグ付け)されますが許可されます。いずれの場合も、そのイベントは interceptions テーブルに書き込まれ、Security → Interceptions に表示されます。
PreToolUse フック(hooks/chronicle-guard.mjs)
このフックは、Chronicle のエンジンを Claude Code の PreToolUse イベントに配線する薄い CLI シムです。フックのペイロードを stdin から読み、{tool_name, tool_input} を POST /api/security/pretooluse に POST し、判定に基づいて動作します。
if (verdict.decision === 'block') {
console.error(verdict.reason); // stderr is shown to the model
process.exit(2); // exit 2 = block the tool call
}
process.exit(0); // allowこれをインストールしても安全である理由は、2 つの設計上の保証です。
- フェイルオープン。 3 秒の
fetchタイムアウトが呼び出しを守ります。Chronicle が実行されていない、あるいはエラーになる、あるいはタイムアウトする場合、フックは0で終了し、ツール呼び出しは手を触れられずに進みます。停止しているときにエディターを壊すセキュリティツールは、ツールがないよりも悪いです。 - まずバックアップ。 ワンクリックのインストーラー(
POST /api/security/install-hook)は、フックを追加する前に~/.claude/settings.jsonをバックアップします。これはデフォルトではインストールされません — オプトインです。エンドポイントはCHRONICLE_URLで上書きできます。
コントリビューターの落とし穴 — 2 つのエラーヒューリスティックを同期させること。 「このツール結果はエラーか?」というチェックは 2 箇所に存在します。
server/api.jsのERROR_RE(プロジェクト分析)とsrc/SessionView.jsxのisErrorResult(Overview の統計)です。一方だけを変更すると Errors のカウントがずれます。エラー検出に手を入れるなら、両方に手を入れてください。
ライブストリーミング(server/live.js)
ライブストリーミングは進行中のセッションを追尾し、新しいメッセージを SSE 経由で開いているビューアーにプッシュします。isLiveCandidate(filePath) は5 分の直近性ウィンドウ(ファイルが直近 5 分以内に書き込まれた)でこれをゲートします。attachLiveStream(sessionId, res) は SSE ストリームを開き、liveStatus() はアクティブなウォッチャーを報告します。
ウォッチャーの実装は 2 種類あり、ソースによって選ばれます。
| ウォッチャー | ソース | 新しいコンテンツの検出方法 |
|---|---|---|
Watcher(JSONL tail) | Claude Code、Codex | stat のサイズポーリング + 最後のオフセットからの差分読み取り。新しい行だけをパースする |
SqlitePollWatcher | Cursor、OpenCode | 一時 DB スナップショットを再パースし、保存されたメッセージ数と差分を取る。WAL を意識した mtime |
JSONL の Watcher はファイルの末尾から開始し(新しいコンテンツだけがストリームされる)、書きかけの末尾行のために partial バッファを保持し、ファイルが切り詰められたりローテートされたりした場合はゼロから再読み取りします。SqlitePollWatcher は外部の DB をライブで開くことは決してなく — パーサーレイヤーがまず一時ディレクトリにスナップショットします — WAL の書き込みがメインファイルに触れないことがあるため mtime を max(db, db-wal) から取ります。両者とも、約 2 分の無音の後にポーリング間隔を遅くし、最後のビューアーが切断すると自動停止します(removeClient → close)。
知っておく価値のある実装上の事実が 2 つあります。
- ウォッチャーは
globalThis.__chronicleLiveに存在するため、Vite SSR のモジュールリロードがポーリングタイマーを孤立させることはありません。 - ライブメッセージは
seqを 1,000,000 から開始することで、保存されたシーケンス番号との衝突を避けます。それらはセッションが再インポートされるまでクライアントの状態にのみ存在します — ライブの追尾はビューであって、DB への書き込みではありません。
この上に載る UI レイヤー(● LIVE インジケーター、指数バックオフの再接続、「N 件の新規メッセージ」ボタン)は ライブストリーミング で扱います。
リプレイエンジン(server/replay.js)
リプレイは、セッションのファイル操作とシェル操作を隔離されたサンドボックス内で再実行し、コードがどのように作られたかを見られるようにします — 決定論的に、LLM 呼び出しなしで、そして実際のプロジェクトに一切触れることなく。
REPLAY_ROOT = ~/.chronicle/replay。各リプレイは ~/.chronicle/replay/<id>/ を得ます。
プラン。 buildPlan(sessionId) はセッションのメッセージを歩き、実行可能なステップ — Write、Edit、Bash のツール呼び出し — を抽出し、直近の assistant/thinking テキストをそのステップの reasoning として付与します。ターゲットパスがプロジェクトの外に出るステップは outOfScope としてフラグを立てます。
サンドボックスのシード。 startReplay(sessionId, workspace) はサンドボックスを消去して作り直し、その後セッション開始時の Git スナップショットからシードします。git エンジン の commitAt() を介して session.started_at のコミットを見つけ、そのツリーを git archive | tar -x でマテリアライズします。したがってリプレイは、AI が触れる前のコードから始まります — 現在のディスクからではありません。
ステップバイステップ。 previewStep(sessionId, seq) は、来たるべき差分を現在のサンドボックス状態に対して計算します(Edit の場合、old_string がまだ applies(適用可能)かどうかまで報告します)。executeStep(sessionId, seq, {confirmCommand}) は 1 ステップを適用します。
- Write / Edit はサンドボックスのパスに直接適用されます(絶対プロジェクトパスはサンドボックスにリマップされ、外に出るパスは例外を投げます)。
- Bash は明示的な
confirmCommandを要求します — それがない場合、executeStepは何も実行せず{ needsConfirmation: true }を返します。コマンドはサンドボックスをcwdおよびHOME(ソフトな封じ込め)として実行され、60 秒のタイムアウトと出力のキャプチャが付きます。
自動再生(1×/2×/5×)はエラーで一時停止し、コマンドステップとプロジェクト外の書き込みをスキップします — ハードに一時停止するのではなく skipped とマークするため、実行が固まって見えることはありません。openWorkspace() はサンドボックスを OS のファイルブラウザで開きます。このファイル内のどこでも、実際のプロジェクトが書き込みの対象になることはありません。リプレイモード を参照してください。
コンテキストの因果関係(server/causality.js)
analyzeCausality(sessionId) は、AI が読んだものと変更したものを、ヒューリスティックな確信度スコアとともに結びつけます — ツール呼び出しシーケンスに対する純粋な構造分析であり、モデルは関与しません。読み取り系のツール呼び出し(Read、Grep、Glob、…)と変更系のツール呼び出し(Write、Edit、…)を収集し、各変更について先行するすべての読み取りをスコアリングします。
| 確信度 | シグナル |
|---|---|
| 0.95 | その後変更したまさにそのファイルを読んだ |
| 0.55 | 同じディレクトリの兄弟ファイルを読んだ |
| 0.5 | 同じベース名のファイルを読んだ |
| 0.45 | 変更されたファイルに一致する検索パターン |
| 0.2 | 変更の直前に読んだ(背景コンテキスト、8 読み取りのウィンドウ内) |
ソースは確信度でソートされ、変更ごとに上限が設けられます。UI 上の Write/Edit メッセージの ⛓ バッジは、これらのソース参照のパネルを開きます。確信度ティアこそが、同じファイルの読み取りが強調され、背景の読み取りが薄く表示される理由です。コンテキストの因果関係 を参照してください。
共有リンク(server/shares.js)
共有は、セッションをトークン化された HTML ページとしてローカルアプリから配信します — 何もアップロードされません。決定的な性質は、リダクションが作成時に凍結されることです。
createShare(sessionId, days = 7) // → { token, url: `/share/${token}`, expires_at, redactions }createShare() はメッセージに対して scanSession() を実行し、リダクション済みコピーのみを shares.content カラムに保存します。元のものが共有に永続化されることは決してないため、後のルール変更 — あるいは誰かが DB を読むこと — が、共有時点でリダクションされたものを漏らすことはできません。listShares() / revokeShare(id) はトークンを管理し(閲覧数、即時失効)、公開ページ(GET /share/:token)は有効期限切れまたは失効すると 404 を返します。デフォルトの有効期間は 7 日です。
なぜすべてがヒューリスティックかつローカルなのか
リダクションの正規表現、ライブ追尾のポーリング、リプレイのファイル操作の再実行、因果関係の構造スコアリングは、すべてコントリビューターが読み、理由づけ、監査できるものです — ネットワークも、推論も、外部依存もありません。それが要点です。すなわち、重い処理はすべてヒューリスティック + ローカルなので、Chronicle はネットワークを抜いても動き続け、その「知性」は不透明ではなく検査可能なのです。
関連ページ
- セキュリティと共有 — セキュリティチェック、カスタムルール、フック、共有リンク。
- ライブストリーミング — LIVE インジケーターと再接続の UX。
- リプレイモード — 決定論的なサンドボックスリプレイのウォークスルー。
- コンテキストの因果関係 — read→change の結びつけと確信度ティア。
- Git スナップショットエンジン — リプレイが履歴からサンドボックスをシードする仕組み。