← コードを読む力

実践コード読解 —— NexusCoreのリアルコミットを読む

自分のプロジェクトの実際のコミットを使った、コード読解の練習教科書。5問付き。


この教科書の使い方

  1. まず自分で考える — diffを見て「何を変更したか」を予想する
  2. 解説を読む — 自分の理解と照らし合わせる
  3. 用語を確認する — 分からない言葉は「用語メモ」を読む

出典: すべてNexusCoreリポジトリの実際のコミット(2026年6月11日)。


問1: セキュリティ修正(最もシンプル)

コミットメッセージ

fix: openrouter_key APIのログから例外詳細を除去(情報漏洩防止)

diff

--- a/src/nexuscore/api/routes/openrouter_key.py
+++ b/src/nexuscore/api/routes/openrouter_key.py
@@ -31,7 +31,7 @@ async def save_openrouter_key(
     try:
         user.openrouter_key_enc = encrypt_string(request.api_key)
     except ValueError as e:
-        logger.error("Encryption failed: %s", e)
+        logger.error("Encryption failed for OpenRouter key storage")
         raise HTTPException(status_code=500, detail="NEXUS_ENCRYPTION_KEY is not configured.")
     db.session.commit()
     return {"message": "OpenRouter key saved"}

自分で考えてみよう


解説

変更内容: エラーログのメッセージから e(例外の詳細)を削除し、固定の文字列に置き換えました。

Before(変更前):

logger.error("Encryption failed: %s", e)

→ エラーの内容(例: "Encryption key is empty")がログに書かれる

After(変更後):

logger.error("Encryption failed for OpenRouter key storage")

→ エラーの事実だけをログに書く。原因の詳細は書かない

なぜ?: ログファイルは開発者以外の人(運用担当者、監査人、攻撃者)が見る可能性があります。暗号化の失敗理由をログに残すと、システムの弱点を攻撃者に教えることになります。これを「情報漏洩」と呼びます。

用語メモ

用語 意味
logger.error() エラーをログファイルに記録するPythonの命令
%s 文字列を埋め込む場所。eの中身がここに入る
HTTPException Web APIで「エラーが起きました」と返す仕組み
encrypt_string() 文字列を暗号化する関数

問2: セキュリティ方針の変更(中難易度)

コミットメッセージ

fix: key_store fail-closed — NEXUS_ENCRYPTION_KEY未設定時は保存拒否

diff(重要部分だけ抜粋)

--- a/src/nexuscore/utils/key_store.py
+++ b/src/nexuscore/utils/key_store.py
@@ -44,33 +43,30 @@ def _flush(keys: dict) -> None:


 def save_key(provider: str, api_key: str) -> None:
-    """Save an encrypted API key for *provider*."""
-    keys = _load_all()
-    try:
-        from nexuscore.utils.crypto_utils import encrypt_string
+    """Save an encrypted API key for *provider*.
+
+    Raises ``ValueError`` if ``NEXUS_ENCRYPTION_KEY`` is not set
+    (fail-closed: never stores keys in plaintext).
+    """
+    from nexuscore.utils.crypto_utils import encrypt_string
+
+    keys = _load_all()
+    keys[provider] = {"encrypted": encrypt_string(api_key)}
     _flush(keys)
-    except ValueError:
-        # NEXUS_ENCRYPTION_KEY not set — plaintext fallback (local dev only)
-        logger.warning("NEXUS_ENCRYPTION_KEY not set; storing %s key in plaintext", provider)
-        keys[provider] = {"plain": api_key}
-        _flush(keys)

テストのdiff

-    def test_save_and_load_plaintext_fallback(self, ...):
+    def test_save_refuses_without_encryption_key(self, ...):
+        """Fail-closed: save_key MUST raise when NEXUS_ENCRYPTION_KEY is not set."""
         monkeypatch.delenv("NEXUS_ENCRYPTION_KEY", raising=False)
-        save_key("openrouter", "sk-or-plain-key")
-        assert load_key("openrouter") == "sk-or-plain-key"
+        with pytest.raises(ValueError, match="NEXUS_ENCRYPTION_KEY"):
+            save_key("openrouter", "sk-or-plain-key")

自分で考えてみよう


解説

変更前の動き(fail-open):

暗号化キーがある? → YES → 暗号化して保存 ✅
                      → NO  → 平文で保存 ⚠️(警告は出る)

変更後の動き(fail-closed):

暗号化キーがある? → YES → 暗号化して保存 ✅
                      → NO  → 保存を拒否(エラーを投げる)❌

「fail-closed」 = 「何か問題が起きたら、安全な方向(閉じる方向)に失敗する」という設計方針です。

具体例: 暗号化キーの設定を忘れた場合、

テストの変更点: 「平文で保存できること」を確認するテストを、「暗号化キーがない場合にエラーになること」を確認するテストに変更しました。

用語メモ

用語 意味
fail-closed エラー時は「安全な方向」に倒す設計方針。ドアで例えると「停電したら鍵がかかったまま」
fail-open エラー時は「緩い方向」に倒す。ドアで例えると「停電したら誰でも通れる」
平文(plaintext) 暗号化されていない、そのまま読めるデータ
pytest.raises(ValueError) 「このエラーが起きるはず」というテストの書き方
monkeypatch.delenv() テスト中だけ環境変数を削除する(「設定がなかったら?」を再現)

問3: 新機能追加(大規模・設計読解)

コミットメッセージ

feat: Gradio UI OpenRouter BYOK設定画面 + 5プロファイル追加

変更統計

 src/nexuscore/llm/llm_profiles.py            | 36 +++++++++++
 src/nexuscore/ui/settings_tab.py             | 69 +++++++++++++++++++++
 src/nexuscore/utils/key_store.py             | 92 ++++++++++++++++++++++++++++
 tests/llm/test_llm_profiles_comprehensive.py |  2 +-
 tests/llm/test_openrouter_provider.py        | 32 ++++++++++
 tests/utils/test_key_store.py                | 80 ++++++++++++++++++++++++
 6 files changed, 310 insertions(+), 1 deletion(-)

設定画面のdiff(重要部分)

def _save_openrouter_key(api_key: str) -> tuple[str, str]:
    """OpenRouterキーを暗号化保存 → (結果メッセージ, ステータス表示)."""
    if not api_key or not api_key.strip():
        return "⚠️ APIキーを入力してください", _openrouter_byok_status()
    try:
        from nexuscore.utils.key_store import save_key
        save_key("openrouter", api_key.strip())
        return "✅ OpenRouter APIキーを保存しました", _openrouter_byok_status()
    except Exception as e:
        return f"❌ 保存エラー: {e}", _openrouter_byok_status()

プロファイルのdiff(抜粋)

# --- OpenRouter BYOK profiles ---
"or_gpt_codex": LLMProfile(
    name="or_gpt_codex",
    provider="openrouter",
    model="openai/gpt-4.1",
    description="GPT-4.1 via OpenRouter — code generation",
    default_temperature=0.2,
),
"or_sonnet": LLMProfile(
    name="or_sonnet",
    provider="openrouter",
    model="anthropic/claude-sonnet-4-6",
    description="Claude Sonnet via OpenRouter — reviews & architecture",
    default_temperature=0.15,
),

自分で考えてみよう


解説

6ファイルの役割分担:

ファイル 役割 レイヤー
llm_profiles.py どのAIモデルを使えるかの定義 設定(Config)
settings_tab.py 設定画面のUIとロジック 画面(UI)
key_store.py APIキーの暗号化保存・読込 データ(Data)
test_llm_profiles_*.py プロファイル定義が正しいか確認 テスト
test_openrouter_provider.py OpenRouter連携が動くか確認 テスト
test_key_store.py キーの保存・読込が正しいか確認 テスト

データの流れ:

① ユーザーが画面にAPIキーを入力
        ↓
② settings_tab.py の _save_openrouter_key() が呼ばれる
        ↓
③ 入力チェック(空文字じゃないか?)
        ↓
④ key_store.py の save_key() が呼ばれる
        ↓
⑤ crypto_utils の encrypt_string() で暗号化
        ↓
⑥ ~/.nexuscore/byok/keys.json に保存

この「画面 → ロジック → データ保存」の流れは、どんなWebアプリでも同じパターンです。これが読めれば、他のプロジェクトのコードも追えるようになります。

用語メモ

用語 意味
tuple[str, str] 2つの文字列をまとめて返す型(例: 結果メッセージとステータス)
LLMProfile AIモデルの設定をまとめたデータ型
provider AIサービスの提供元(OpenAI, Anthropic, Google等)
temperature AIの出力の「ランダムさ」。0に近いほど決定的、高いほど多様
BYOK Bring Your Own Key — ユーザーが自分のAPIキーを持ち込む仕組み

問4: Web画面の追加(HTML読解)

コミットメッセージ

feat: OpenRouterキー登録UI追加(/settings/)

テンプレートHTMLのdiff(抜粋)

<p>
  現在の状態:
  {% if key_configured %}
    <span class="status-configured">✅ 登録済み</span>
  {% else %}
    <span class="status-not-configured">未登録</span>
  {% endif %}
</p>

<form method="post" action="/settings/openrouter-key">
  <div class="form-row">
    <input type="password" name="api_key" placeholder="sk-or-xxxx" required />

Flask Blueprintの登録

-    from nexuscore.webapp import views_dashboard, views_logs, views_projects
+    from nexuscore.webapp import views_dashboard, views_logs, views_projects, views_settings

     app.register_blueprint(views_settings.bp)

自分で考えてみよう


解説

{% if key_configured %}: Jinja2テンプレートの「条件分岐」です。

key_configured が True  → 「✅ 登録済み」を表示
key_configured が False → 「未登録」を表示

これはWeb画面でよく使われるパターン「状態に応じて表示を変える」です。

type="password": 入力欄に入力した文字が ●●●● で隠されます。APIキーは秘密情報なので、画面にそのまま表示してはいけません。type="text" だと入力したキーが丸見えになります。

register_blueprint: Flask(PythonのWeb framework)で「このURLパターンはこのファイルが担当する」と登録する仕組みです。/settings/ にアクセスが来たら views_settings.py が処理します。


問5: ドキュメント追加(全体像の理解)

コミットメッセージ

docs: OpenRouter BYOK セットアップガイドと.env.template追記

変更内容

 .env.template                       |  3 ++
 docs/setup/openrouter_byok_setup.md | 93 +++++++++++++++++++++++++++++
 2 files changed, 96 insertions(+), 1 deletion(-)

.env.templateのdiff

+# OpenRouter BYOK: python -c "from nexuscore.utils.crypto_utils import generate_encryption_key; print(generate_encryption_key())"
+NEXUS_ENCRYPTION_KEY=
+OPENROUTER_API_KEY=

自分で考えてみよう


解説

コードではなく「設定ファイルのひな形」と「説明書」を追加したコミットです。

.env.template は「必要な環境変数の一覧」です。= の右側が空なのは、実際の値は各ユーザーが自分で設定するからです。これは「秘密情報をGitにコミットしない」というセキュリティルールの実践です。

.env.template → 「NEXUS_ENCRYPTION_KEY= が必要ですよ」という案内
.secrets.env  → 実際の値(NEXUS_ENCRYPTION_KEY=abc123...)。Gitには含めない

5つのコミットの全体像:

問5 ドキュメント追加(説明書を書いた)
  ↓
問4 Web画面追加(設定ページを作った)
  ↓
問3 新機能追加(画面・プロファイル・テストを一気に追加)
  ↓
問2 セキュリティ方針変更(平文保存を廃止)
  ↓
問1 ログの修正(エラー詳細をログに残さない)

→ この5つを上から下に読むと、「OpenRouter BYOK機能を追加して、セキュリティを固めた」1つのストーリーが見えます。


振り返り

読めるようになったこと

読んだ内容 身につく力
問1 1行の変更 ログに何を書いていいか/悪いかの判断
問2 セキュリティ方針の変更 fail-closed vs fail-open の理解
問3 6ファイル・310行の追加 画面→ロジック→データの流れ
問4 HTMLテンプレート 画面のコードの基本パターン
問5 ドキュメント追加 コミットの全体像(ストーリー)

次のステップ

  1. 自分の他のリポジトリ(atelier-kyo-manager等)でも同じ練習をする
  2. エラーメッセージを読む練習pytest の失敗出力を読んでみる
  3. PRのdiffを読む練習git diff main..feature-branch を読んでみる