実践コード読解 —— NexusCoreのリアルコミットを読む
自分のプロジェクトの実際のコミットを使った、コード読解の練習教科書。5問付き。
この教科書の使い方
- まず自分で考える — diffを見て「何を変更したか」を予想する
- 解説を読む — 自分の理解と照らし合わせる
- 用語を確認する — 分からない言葉は「用語メモ」を読む
出典: すべて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"}
自分で考えてみよう
- 何が変わった?(1行だけ注目)
- なぜ変わった?(コミットメッセージのヒント: 「情報漏洩防止」)
解説
変更内容: エラーログのメッセージから 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-closed」とは何か?(ヒント: 辞書で "fail" と "closed" を組み合わせて想像してみよう)
- 変更前の動きと変更後の動きの違いは?
解説
変更前の動き(fail-open):
暗号化キーがある? → YES → 暗号化して保存 ✅
→ NO → 平文で保存 ⚠️(警告は出る)
変更後の動き(fail-closed):
暗号化キーがある? → YES → 暗号化して保存 ✅
→ NO → 保存を拒否(エラーを投げる)❌
「fail-closed」 = 「何か問題が起きたら、安全な方向(閉じる方向)に失敗する」という設計方針です。
具体例: 暗号化キーの設定を忘れた場合、
- fail-open: 平文で保存してしまう → APIキーが丸見え → 危険!
- fail-closed: 保存を拒否する → APIキーは保存されない → 安全
テストの変更点: 「平文で保存できること」を確認するテストを、「暗号化キーがない場合にエラーになること」を確認するテストに変更しました。
用語メモ
| 用語 | 意味 |
|---|---|
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ファイルの役割を整理してみよう)
- データの流れ: ユーザーが画面でキーを入力してから、暗号化されて保存されるまでの流れを追ってみよう
解説
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 %}は何をしている?(ヒント:ifという言葉に注目)type="password"はなぜtype="text"ではないのか?
解説
{% 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に=の右側が空の状態で追加したのか?(実際の値はどこに書く?)
解説
コードではなく「設定ファイルのひな形」と「説明書」を追加したコミットです。
.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 | ドキュメント追加 | コミットの全体像(ストーリー) |
次のステップ
- 自分の他のリポジトリ(atelier-kyo-manager等)でも同じ練習をする
- エラーメッセージを読む練習 —
pytestの失敗出力を読んでみる - PRのdiffを読む練習 —
git diff main..feature-branchを読んでみる