AIレビューが出した「新規の P1」を追いかけたら、1ヶ月前の自分のメモに同じことが書いてあった、という記録です。直す対象がコードより先に「寝かせ方」だったので、そっちを書きます。記事を書いた後で被害の見積もりが間違っていたと分かったので、その訂正も最後に入れました。
ここで言う「web」は、綴ったものを web でも見られるように開発中のアプリ(紡Book)のことです。
Codex に全体レビューを一周させたら、P1(リリースを止める級)が4件返ってきた。そのうちの1件がこれ。
macOS Release に loopback OAuth 用の server entitlement がない
読んで、見覚えがあった。1ヶ月前の自分の設計書に載っている。
まず何が抜けていたか
ログインの仕組みはこうなっています。アプリは Google を直接触らず、自分の web のログインページをブラウザで開いて、認証が終わったら結果を受け取る。その「受け取る」ために、アプリが一瞬だけ自分でサーバになる。
// lib/core/auth/oauth/redirect_capture.dart
// ブラウザからの戻り(?code=...)を受けるため、自分自身が待ち受けポートを開く。
// ポート番号 0 = OS に空いてるやつを1個くれ、の意味。
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);問題はここ。macOS のサンドボックスでは、外へ繋ぎに行くのと待ち受けるのが別の権限になっています。
2つのファイルを並べるとこうなっていた。
<!-- DebugProfile.entitlements(開発用)— これはある -->
<key>com.apple.security.network.server</key> <!-- 待ち受ける(自分がサーバになる)許可 -->
<true/>
<key>com.apple.security.network.client</key> <!-- 外へ繋ぎに行く許可 -->
<true/>
<!-- Release.entitlements(配布用)— server が無い -->
<key>com.apple.security.network.client</key> <!-- 外へ出るぶんだけ。待ち受け側が無い -->
<true/>ここで一つ言っておきたいのは、雑にやった結果ではないということ。Release 側のファイルは、他のキーには「なぜ要るのか」のコメントが付いている。フォントを実行時に取るから client が要る、ピッカーで選んだファイルを読むから user-selected が要る、再起動後も読めるように bookmark が要る。Debug 側には「Release.entitlements と揃えて」と2箇所も書いてある。
2つのファイルを揃える意識はあった。揃え損ねた1個は、他とは違うタイミングで、違う理由で足されたやつだった。だから「次からちゃんと見る」は対策になりません。ちゃんと見た結果がこれなので。
なお、署名済みの配布ビルドで実際に待ち受けが失敗するところは、まだ見ていない。Mac が手元にないと確かめられない。サンドボックスの仕様からはそうなるはず、という段階の話として読んでほしい。
「新規の P1」は1ヶ月前の自分のメモに載っていた
6月末に一度、同じように全体レビューをかけて実装設計書を作っている。その中の未消化リストにこう書いてあった。
残=OAuth 戻り口(mobile)と macOS entitlement は
「App Links 移行」で一体化し着手保留
自分で見つけて、自分で「あとでやる」に入れて、そのまま1ヶ月置いた。忘れたのではなく、置いた場所が悪かった。
束ねた2つは、別のトリガーで動くものだった
「一体化」した2つを並べてみる。
- OAuth 戻り口(App Links) — Android と iOS の話。ブラウザから
https://...のリンクでアプリを開き直す仕組みです。実装するには Android の署名鍵を作るか、iOS を TestFlight に上げるか、サーバに本番設定を置くかが要ります。 - macOS entitlement — デスクトップの話。上に書いた通り、待ち受けの権限を1行足すだけです。
書き出すと明らかに別物なのですが、当時は「どっちもログインの戻り道の話だから」で1つのブロックにした。それらしく見えたし、実際どちらも認証の周辺ではあります。
「あとでやる」に入れるとき、再開の条件(トリガー)も一緒に書いてあった。3つ。それぞれが macOS entitlement に掛かるかを、いま1個ずつ照合するとこうなります。
引っかかるのは、この照合を当時やらなかったこと自体ではない。照合するという発想が無かったことのほうだ。トリガーは束に対して1回書くもので、束の中身1つずつに対して書くものだとは思っていなかった。
保留に入れた瞬間、それは在庫から消える
もっと効いたのはこっち。
自分は「未対応の残タスク」を数えて、溜まりすぎたら実装をやめて検証に回す、というルールで動いています。だから1ヶ月、誰も——レビューも、自分の目も——ここを見なかった。7月も自分で触ってはいたが、触っていたのは Windows 版なので踏みません。
記事を書いた後で、被害の見積もりが間違っていたと分かった
最初この記事には「出荷済みの Mac 版が1ヶ月ログインできなかった」と書いていた。タイトルもそれだった。
書き上げた後、実装を進めているときに「本当にそうなの?」と聞かれて、日付を突き合わせた。
- Mac の dmg を配ったのが 6月11日
- ログインの実装が入ったのが 6月24日
引っかかったのは、同じレビューの中で Windows について全く同じ訂正を自分でしていたこと。「今ストアに出ている版は起動不能」と書きかけて、公開日とガードを足した日を比べて「壊れるのは次に出す版」と直した。その数時間後に、macOS で同じことをやった。片方は確かめて、片方は確かめなかった。
どちらも再検査の対象から外れる操作だった点で、この記事が扱っている「保留に入れたものは誰も見ない」と形が同じだった。
抜けていた権限も、束ね方の事故も、事実は何も変わりません。変わったのは深刻さの区分だけです。ただ、その区分は「何から先に直すか」を決めるものなので、盛ると直す順番が歪みます。
束ねたら、トリガーを項目ごとに聞き直す
割ってさえいれば macOS 側はその日のうちに終わっています。権限を1行足すだけなので5分もかかりません。
判定の軸も置き直した。「もう誰かが使っているか」ではなく「次に配るものに載るか」。載るなら「あとで」ではなく、数えられる側に戻します。
で、これをどこに置くか
上のルールは、書いた時点ではただの反省文で、次に寝かせるときに思い出す保証がどこにもありません。1ヶ月前の自分も、たぶん同じくらい真剣にやっていた。
なので置き場所として出てきたのが CI だった。Debug にあって Release に無い認証まわりの権限があったら、そこで落ちます。寝かせ方をまた間違えても、次のプッシュで引っかかります。
同じ移し方は7月11日に一度やっていて、そのときは「既存の違反は責めず、新しい違反だけ止める — アーキテクチャ検査を一方向ラチェットにする」に書いた通り、既存の34箇所は allowlist に記録して見逃したまま、新しく増えた違反だけを止める形にした。今回の権限の検査はまだ入れていないので、ここは置き場所が同じだったという話にとどまる。
人が覚えて守るルールを、全員が必ず通る出口に移します。この日に見えていたのはここまでで、何をどこまで検査させるかはまだ決めていない。
見積もりを5箇所にコピーした話も同じで、気づけたのは注意力が戻ったからではない。実装中に「本当にそうなの?」と聞かれて、日付を突き合わせたからだった。
この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。