<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>AI作業日誌 on sonohen's tech blog</title><link>https://sonohen.net/categories/ai%E4%BD%9C%E6%A5%AD%E6%97%A5%E8%AA%8C/</link><description>Recent content in AI作業日誌 on sonohen's tech blog</description><generator>Hugo -- gohugo.io</generator><language>ja-JP</language><lastBuildDate>Mon, 12 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://sonohen.net/categories/ai%E4%BD%9C%E6%A5%AD%E6%97%A5%E8%AA%8C/index.xml" rel="self" type="application/rss+xml"/><item><title>DockerのAppArmor適用失敗：プロファイルの読み込みとreadby権限で対処</title><link>https://sonohen.net/posts/2026/10/12/</link><pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate><guid>https://sonohen.net/posts/2026/10/12/</guid><description>&lt;p&gt;UbuntuでCodex用のDockerコンテナを起動したところ、AppArmorプロファイルの適用に失敗した。原因は、起動時に指定したカスタムプロファイルがホストに読み込まれていなかったことだった。読み込むとコンテナは起動したが、続いてコンテナ内のbubblewrapで権限不足が見つかり、プロファイルの修正も必要になった。&lt;/p&gt;
&lt;p&gt;この記事では、Dockerを使う技術者に向けて、拒否ログによる切り分けと対処を説明する。今回確認した問題は次の2段階だった。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;発生段階&lt;/th&gt;
 &lt;th&gt;ログの手掛かり&lt;/th&gt;
 &lt;th&gt;対処&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;コンテナの起動時&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;label not found&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;指定したプロファイルをホストに読み込む&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;起動後のbubblewrap実行時&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;denied_mask=&amp;quot;readby&amp;quot;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;同じプロファイルのプロセス間に&lt;code&gt;readby&lt;/code&gt;を許可する&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;調査はCodexに依頼し、ホストの設定変更とコマンド実行は自分で行った。修正後はコンテナへのプロファイル適用を確認し、提示した直近5分のログに拒否はなかった。その後、ホストの再起動後も自分で確認し、問題なかった。&lt;/p&gt;
&lt;h2 id="この環境でのapparmorとbubblewrapの役割"&gt;この環境でのAppArmorとbubblewrapの役割
&lt;/h2&gt;&lt;p&gt;AppArmorは、プロセスが実行できる操作をプロファイルで制限するLinuxのセキュリティ機構だ。今回の環境では、Dockerコンテナに&lt;code&gt;codex-bwrap&lt;/code&gt;というカスタムプロファイルを適用していた。これは自分の環境で用意した名前で、Docker標準のプロファイル名ではない。&lt;/p&gt;
&lt;p&gt;コンテナ内では、Codexのコマンド実行を制限するためにbubblewrap（実行ファイル名は&lt;code&gt;bwrap&lt;/code&gt;）も使っている。コンテナが起動した後も、その中のプロセスはAppArmorの制約を受けるため、bubblewrapの動作に必要な操作をプロファイルで許可する必要がある。&lt;/p&gt;
&lt;p&gt;また、プロファイルには次の3つの状態がある。設定ファイルがあるだけでは、コンテナに適用できない。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;状態&lt;/th&gt;
 &lt;th&gt;今回の対象・確認方法&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;設定ファイルが存在する&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;~/work/codex-docker/codex-bwrap.apparmor&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ホストのLinux Kernelに読み込まれている&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;apparmor_parser&lt;/code&gt;で読み込み、&lt;code&gt;aa-status&lt;/code&gt;で確認&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;コンテナに適用されている&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;docker inspect&lt;/code&gt;の&lt;code&gt;AppArmorProfile&lt;/code&gt;で確認&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以下のコマンドは、Dockerデーモンが動いているUbuntuホストの通常のユーザー端末で実行する。パスとプロファイル名は自分の環境に合わせて読み替える必要がある。掲載するルールは既存プロファイルの変更箇所であり、プロファイル全体ではない。&lt;/p&gt;
&lt;h2 id="発生したエラーと環境"&gt;発生したエラーと環境
&lt;/h2&gt;&lt;p&gt;実行したのは、自作のCodex用Docker起動スクリプトだ。このスクリプトは、コンテナの起動時に&lt;code&gt;codex-bwrap&lt;/code&gt;を指定する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;~/work/codex-docker/run-codex.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;出力は次のとおりだった。末尾のヘルプ案内は省略している。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Docker AppArmor setting: codex-bwrap
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: write fsmount:fscontext:proc/thread-self/attr/apparmor/exec: no such file or directory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;unable to apply apparmor profile&lt;/code&gt;から、コンテナを準備するOCIランタイムがAppArmorを適用する段階で失敗したと分かる。コンテナ内のCodexは、まだ動き始めていなかった。調査時に確認した環境は次のとおり。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;項目&lt;/th&gt;
 &lt;th&gt;バージョン・状態&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Ubuntu&lt;/td&gt;
 &lt;td&gt;26.04.1 LTS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Linux Kernel&lt;/td&gt;
 &lt;td&gt;7.0.0-38-generic&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Docker Client / Server&lt;/td&gt;
 &lt;td&gt;29.9.0 / 29.9.0&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Docker context&lt;/td&gt;
 &lt;td&gt;default&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;containerd&lt;/td&gt;
 &lt;td&gt;v2.4.1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;runc&lt;/td&gt;
 &lt;td&gt;1.5.2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;AppArmor parser&lt;/td&gt;
 &lt;td&gt;5.0.2&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;AppArmor&lt;/td&gt;
 &lt;td&gt;有効、&lt;code&gt;codex-bwrap&lt;/code&gt;を指定&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="起動時の拒否ログからプロファイル未読み込みを確認する"&gt;起動時の拒否ログから、プロファイル未読み込みを確認する
&lt;/h2&gt;&lt;p&gt;エラー末尾は&lt;code&gt;no such file or directory&lt;/code&gt;だが、Codexが確認すると、ホストの&lt;code&gt;/proc/thread-self/attr/apparmor/exec&lt;/code&gt;は存在していた。&lt;/p&gt;
&lt;p&gt;原因を判断する手掛かりになったのは、Linux Kernelのログだった。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo aa-status | rg &lt;span style="color:#e6db74"&gt;&amp;#39;codex-bwrap&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo journalctl -k -b --no-pager --grep&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;codex-bwrap|label not found&amp;#39;&lt;/span&gt; -n &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;aa-status&lt;/code&gt;の抽出結果に&lt;code&gt;codex-bwrap&lt;/code&gt;の行はなく、ログには次の拒否が記録されていた。時刻やホスト名などを省略し、メッセージ部分を抜粋している。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apparmor=&amp;#34;DENIED&amp;#34; operation=&amp;#34;change_onexec&amp;#34; class=&amp;#34;file&amp;#34; info=&amp;#34;label not found&amp;#34; error=-2 profile=&amp;#34;runc&amp;#34; name=&amp;#34;codex-bwrap&amp;#34; pid=8853 comm=&amp;#34;runc:[2:INIT]&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ここでいうラベルは、AppArmorがプロファイルを識別するための名前だ。&lt;code&gt;name=&amp;quot;codex-bwrap&amp;quot;&lt;/code&gt;と&lt;code&gt;info=&amp;quot;label not found&amp;quot;&lt;/code&gt;から、起動時に指定したプロファイルを見つけられなかったと分かる。&lt;a class="link" href="https://raw.githubusercontent.com/opencontainers/runc/v1.5.2/libcontainer/apparmor/apparmor_linux.go" target="_blank" rel="noopener"
 &gt;runc 1.5.2の実装&lt;/a&gt;では、exec属性への書き込みでプロファイルを指定する。今回の書き込みエラーは、パス自体が存在しないという意味には限定できなかった。&lt;/p&gt;
&lt;p&gt;Codexが起動スクリプトを調べたところ、AppArmorを自動選択する&lt;code&gt;CODEX_APPARMOR=auto&lt;/code&gt;設定は、DockerのAppArmor対応だけを確認していた。カスタムプロファイルの登録確認や読み込みは行っていなかった。&lt;a class="link" href="https://docs.docker.com/engine/security/apparmor/" target="_blank" rel="noopener"
 &gt;Dockerの公式資料&lt;/a&gt;でも、カスタムプロファイルは先に読み込み、その後でコンテナに指定する手順になっている。&lt;/p&gt;
&lt;p&gt;今回の&lt;code&gt;aa-status&lt;/code&gt;と拒否ログを合わせると、指定したプロファイルをホストに読み込む必要があった。ただし、以前の登録が再起動などで失われたのか、別の理由で未読み込みだったのかは特定できていない。&lt;/p&gt;
&lt;h2 id="プロファイルを読み込んで起動エラーを解消した"&gt;プロファイルを読み込んで起動エラーを解消した
&lt;/h2&gt;&lt;p&gt;Codexがプロファイルの実ファイルを確認し、自分で次のコマンドを実行した。&lt;code&gt;apparmor_parser&lt;/code&gt;は設定ファイルを解析してホストに読み込むコマンドで、&lt;code&gt;-r&lt;/code&gt;は既存のプロファイルがあれば置き換える指定だ。&lt;/p&gt;
&lt;p&gt;ここではファイルの場所が明確になるよう、絶対パスを使っている。管理者権限の実行環境では、&lt;code&gt;$HOME&lt;/code&gt;が通常ユーザーのホームを指すとは限らない。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apparmor_parser -r -W /home/sonohen/work/codex-docker/codex-bwrap.apparmor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo aa-status | rg &lt;span style="color:#e6db74"&gt;&amp;#39;codex-bwrap&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;実行した結果、&lt;code&gt;apparmor_parser&lt;/code&gt;の終了コードは0となり、&lt;code&gt;aa-status&lt;/code&gt;にも&lt;code&gt;codex-bwrap&lt;/code&gt;が表示された。コンテナの&lt;code&gt;docker inspect&lt;/code&gt;でも同じプロファイル名を確認でき、元のOCI起動エラーは解消した。&lt;/p&gt;
&lt;h2 id="起動後の拒否ログからreadby権限を追加する"&gt;起動後の拒否ログから、readby権限を追加する
&lt;/h2&gt;&lt;p&gt;コンテナへの適用を確認した後、Linux Kernelのログに別の拒否が出ていた。&lt;code&gt;comm=&amp;quot;bwrap&amp;quot;&lt;/code&gt;はbubblewrapのプロセスを示す。最初の起動エラーが解消しても、プロファイルの内容が実行時の操作に足りるとは限らなかった。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apparmor=&amp;#34;DENIED&amp;#34; operation=&amp;#34;ptrace&amp;#34; class=&amp;#34;ptrace&amp;#34; profile=&amp;#34;codex-bwrap&amp;#34; pid=11588 comm=&amp;#34;bwrap&amp;#34; requested_mask=&amp;#34;readby&amp;#34; denied_mask=&amp;#34;readby&amp;#34; peer=&amp;#34;codex-bwrap&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当時のプロファイルには、次のルールがあった。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ptrace (read, trace) peer=codex-bwrap,
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このログでは、&lt;code&gt;profile&lt;/code&gt;と&lt;code&gt;peer&lt;/code&gt;がともに&lt;code&gt;codex-bwrap&lt;/code&gt;で、拒否された権限は&lt;code&gt;readby&lt;/code&gt;だ。同じプロファイルが適用されたプロセス同士のアクセスで、読まれる側の許可が不足していた。&lt;/p&gt;
&lt;p&gt;AppArmorの&lt;code&gt;ptrace&lt;/code&gt;ルールは、デバッガによる操作に加え、一部の&lt;code&gt;/proc&lt;/code&gt;経由のプロセス情報へのアクセスも制御する。&lt;code&gt;read&lt;/code&gt;は相手の情報を読む側、&lt;code&gt;readby&lt;/code&gt;は相手に読まれる側の許可を表す。&lt;a class="link" href="https://manpages.ubuntu.com/manpages/resolute/man5/apparmor.d.5.html" target="_blank" rel="noopener"
 &gt;Ubuntu 26.04のAppArmorマニュアル&lt;/a&gt;でも、アクセスする側と受ける側の双方に許可が必要とされる。同じプロファイルのプロセス間でも、&lt;code&gt;read&lt;/code&gt;だけでは足りなかった。&lt;/p&gt;
&lt;p&gt;Codexの提案に従い、元ファイル&lt;code&gt;/home/sonohen/work/codex-docker/codex-bwrap.apparmor&lt;/code&gt;の該当ルールを次のように変更した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ptrace (read, readby, trace) peer=codex-bwrap,
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;許可対象は&lt;code&gt;peer=codex-bwrap&lt;/code&gt;のままにした。今回の拒否ログでは&lt;code&gt;tracedby&lt;/code&gt;は要求されていないため、追加していない。&lt;/p&gt;
&lt;h2 id="修正版をホストへ配置し再起動後も読み込めるようにする"&gt;修正版をホストへ配置し、再起動後も読み込めるようにする
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;apparmor_parser&lt;/code&gt;による読み込みは、現在動いているLinux Kernelに対する操作だ。再起動後も読み込めるよう、修正版のファイルをホストのAppArmor設定ディレクトリ&lt;code&gt;/etc/apparmor.d/codex-bwrap&lt;/code&gt;へ配置した。&lt;/p&gt;
&lt;p&gt;作業時には、初回の配置で宛先ファイルが存在しないことを確認していた。その後の&lt;code&gt;readby&lt;/code&gt;追加では、配置済みのファイルをバックアップしてから更新した。以下の掲載例は、既存ファイルがある場合だけバックアップする形にまとめている。&lt;/p&gt;
&lt;p&gt;バックアップは&lt;code&gt;/var/backups&lt;/code&gt;に置く。元の作業手順では&lt;code&gt;/etc/apparmor.d&lt;/code&gt;内に置いていたが、&lt;a class="link" href="https://gitlab.com/apparmor/apparmor/-/raw/v5.0.2/libraries/libapparmor/src/private.c" target="_blank" rel="noopener"
 &gt;AppArmor 5.0.2の実装&lt;/a&gt;では、&lt;code&gt;.bak-日時&lt;/code&gt;は読み込みの除外対象にならない。古いファイルも読み込み対象になり得るため、掲載例では配置先を変更した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo install -d -m &lt;span style="color:#ae81ff"&gt;0700&lt;/span&gt; /var/backups/codex-apparmor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt; -f /etc/apparmor.d/codex-bwrap &lt;span style="color:#f92672"&gt;]&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sudo cp -a /etc/apparmor.d/codex-bwrap &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /var/backups/codex-apparmor/codex-bwrap.bak-&lt;span style="color:#66d9ef"&gt;$(&lt;/span&gt;date +%Y%m%d-%H%M%S&lt;span style="color:#66d9ef"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo install -m &lt;span style="color:#ae81ff"&gt;0644&lt;/span&gt; /home/sonohen/work/codex-docker/codex-bwrap.apparmor &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /etc/apparmor.d/codex-bwrap
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apparmor_parser -r -W /etc/apparmor.d/codex-bwrap
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo aa-status | rg &lt;span style="color:#e6db74"&gt;&amp;#39;codex-bwrap&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ホストのポリシー変更、配置、再読み込みは自分で実施した。Codexは元ファイルの&lt;code&gt;readby&lt;/code&gt;追加と、配置済みファイルとの一致を読み取りで確認した。&lt;/p&gt;
&lt;h2 id="適用結果と再起動後の確認"&gt;適用結果と再起動後の確認
&lt;/h2&gt;&lt;p&gt;配置と再読み込みが終わったら、コンテナを起動して適用結果を確認する。自作スクリプトの&lt;code&gt;CODEX_APPARMOR=required&lt;/code&gt;は、AppArmorを必須にする設定だ。作業対象のディレクトリで次を実行する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CODEX_APPARMOR&lt;span style="color:#f92672"&gt;=&lt;/span&gt;required ~/work/codex-docker/run-codex.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;起動後、別の端末から対象コンテナと拒否ログを確認する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker ps
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker inspect --type container --format &lt;span style="color:#e6db74"&gt;&amp;#39;{{.AppArmorProfile}}&amp;#39;&lt;/span&gt; CONTAINER_ID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo journalctl -k --since &lt;span style="color:#e6db74"&gt;&amp;#39;5 minutes ago&amp;#39;&lt;/span&gt; --no-pager &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --grep&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;codex-bwrap|apparmor=&amp;#34;DENIED&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;CONTAINER_ID&lt;/code&gt;には、&lt;code&gt;docker ps&lt;/code&gt;で確認した実際のIDを指定する。期待する結果は、コンテナが起動し、&lt;code&gt;AppArmorProfile&lt;/code&gt;が&lt;code&gt;codex-bwrap&lt;/code&gt;となり、対象の操作時に新しい拒否ログが出ないことだ。確認に5分以上かかる場合は、&lt;code&gt;--since&lt;/code&gt;の範囲を起動時刻まで広げる。&lt;/p&gt;
&lt;p&gt;自分が提示した修正後の結果では、&lt;code&gt;docker inspect&lt;/code&gt;の出力は&lt;code&gt;codex-bwrap&lt;/code&gt;だった。直近5分のログには次の更新通知だけがあり、&lt;code&gt;DENIED&lt;/code&gt;はなかった。メッセージ部分を抜粋する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apparmor=&amp;#34;STATUS&amp;#34; operation=&amp;#34;profile_replace&amp;#34; profile=&amp;#34;unconfined&amp;#34; name=&amp;#34;codex-bwrap&amp;#34; pid=12838 comm=&amp;#34;apparmor_parser&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ここにある&lt;code&gt;profile=&amp;quot;unconfined&amp;quot;&lt;/code&gt;は、更新を行ったプロセス側の表示だ。コンテナに適用されたプロファイルは&lt;code&gt;docker inspect&lt;/code&gt;の結果で確認した。&lt;/p&gt;
&lt;p&gt;ここまでの適用結果とログは調査記録に残っている。再起動後の自動読み込みは、その記録をまとめた時点では未確認だったが、その後、自分でホストを再起動して確認し、問題なかった。&lt;/p&gt;
&lt;p&gt;ただし、当初行おうとしていたCodexの具体的な操作の成否は、調査記録には残っていない。コンテナ起動とAppArmor適用の確認だけで、Codexの全機能が動作したとは判断していない。また、修正後に拒否がなかったという判断は、提示したログの時間範囲に基づく。&lt;/p&gt;
&lt;h2 id="同じ症状を調べるときの確認順序"&gt;同じ症状を調べるときの確認順序
&lt;/h2&gt;&lt;p&gt;今回の調査では、エラー末尾の&lt;code&gt;no such file or directory&lt;/code&gt;だけでは原因を判断できなかった。Linux Kernelのログでラベル解決失敗を確認し、読み込み後には別の権限不足を切り分けた。同じ症状を調べる場合は、次の順序で確認すると、今回の2つの問題を区別できる。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;aa-status&lt;/code&gt;と起動時の拒否ログで、指定したプロファイルが読み込まれているか確認する。&lt;/li&gt;
&lt;li&gt;起動できたら、&lt;code&gt;docker inspect&lt;/code&gt;でコンテナへの適用を確認する。&lt;/li&gt;
&lt;li&gt;対象の操作を実行し、新しい拒否ログの&lt;code&gt;profile&lt;/code&gt;、&lt;code&gt;peer&lt;/code&gt;、&lt;code&gt;denied_mask&lt;/code&gt;から不足している許可を調べる。&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>CodexでレシートPDFからBeancountへの仕訳登録を自動化した</title><link>https://sonohen.net/posts/2026/10/11-beancount-receipts/</link><pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate><guid>https://sonohen.net/posts/2026/10/11-beancount-receipts/</guid><description>&lt;p&gt;PDFで保存したレシートを、Beancountの帳簿へ転記する作業をCodexに任せることにした。既存の勘定科目を使って仕訳案を作り、人間が確認したら帳簿へ書き込む。登録するたびに&lt;code&gt;bean-check&lt;/code&gt;を実行し、成功するまで次の取引へ進まない運用にした。&lt;/p&gt;
&lt;p&gt;自動化したのは読取り、仕訳案の作成、証跡の整理、帳簿への転記と検証である。仕訳案の最終確認は人間が行う。ファイル監視で無人処理する仕組みにはしていない。&lt;/p&gt;
&lt;p&gt;全体の流れは次のとおりである。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;人間がレシートをPDFで保存し、ファイル名に支払方法を付ける。&lt;/li&gt;
&lt;li&gt;Codexが全ページを読み、既存科目と支払方法から仕訳案を作る。&lt;/li&gt;
&lt;li&gt;人間が仕訳案と証跡の組合せを確認する。&lt;/li&gt;
&lt;li&gt;補助ツールが証跡を保存し、1取引ずつ帳簿へ登録する。&lt;/li&gt;
&lt;li&gt;各登録の直後に&lt;code&gt;bean-check&lt;/code&gt;を実行し、成功したら次へ進む。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="codexが判断しpythonが書き込む"&gt;Codexが判断し、Pythonが書き込む
&lt;/h2&gt;&lt;p&gt;帳簿は次の構成で管理している。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;ファイル&lt;/th&gt;
 &lt;th&gt;役割&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;account.beancount&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;勘定科目と日本語の名称&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;year/2026.beancount&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;2026年の仕訳&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;main.beancount&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;科目、各年の仕訳などを読み込む入口&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Codexには、レシートの内容に加えて&lt;code&gt;account.beancount&lt;/code&gt;と既存の仕訳を読ませる。店舗名だけで費用科目を決めず、酒類、書籍、レジ袋などを品目に応じて分ける。消費税も既存の記帳方法に合わせて別科目へ計上する。&lt;/p&gt;
&lt;p&gt;読取りと判断はCodex、金額の検証とファイル操作はPythonの補助ツールに分担した。補助ツール自体にはOCRやAI APIの呼出しを組み込んでいない。&lt;/p&gt;
&lt;p&gt;実装環境はPython 3、Beancount 3.2.0、Popplerである。手順はリポジトリの&lt;code&gt;AGENTS.md&lt;/code&gt;に記載し、補助ツールを&lt;code&gt;scripts/receipt.py&lt;/code&gt;として作成した。&lt;/p&gt;
&lt;h2 id="抽出テキストだけで転記しない"&gt;抽出テキストだけで転記しない
&lt;/h2&gt;&lt;p&gt;スキャンしたPDFには抽出可能なテキストが入っていても、数字や店舗名に誤読があった。そこで、&lt;a class="link" href="https://manpages.debian.org/trixie/poppler-utils/pdftotext.1.en.html" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pdftotext&lt;/code&gt;&lt;/a&gt;で抽出した内容を、&lt;a class="link" href="https://manpages.debian.org/trixie/poppler-utils/pdftoppm.1.en.html" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pdftoppm&lt;/code&gt;&lt;/a&gt;で画像化した全ページと照合する。取引日、合計、税額、割引は画像でも確認する。&lt;/p&gt;
&lt;p&gt;実際に、ファイル名の日付と取引日が違うレシートがあった。ファイル名が&lt;code&gt;2026-10-11&lt;/code&gt;で始まるPDFに、10月8日の給油が記載されていた。別のPDFも、ファイル名は&lt;code&gt;2026-10-11&lt;/code&gt;で始まるが取引日は10月9日だった。&lt;/p&gt;
&lt;p&gt;帳簿の日付にはレシートの取引日を使う。読めない数字や証跡間の矛盾があれば、推測して登録せず人間に質問させる。&lt;/p&gt;
&lt;h2 id="支払方法はファイル名に書く"&gt;支払方法はファイル名に書く
&lt;/h2&gt;&lt;p&gt;最初は、レシートを読むたびに支払方法を質問させていた。カード払いと分かっても、どのカードかは帳簿の科目へ対応させる必要があるためだ。&lt;/p&gt;
&lt;p&gt;毎回回答する手間を減らすため、自分でPDF名の末尾に支払方法を付けることにした。次のような形式である。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;日付.店舗名.支払方法.pdf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;次の表は対応の一例で、今回使った支払方法を示している。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;ファイル名の支払方法&lt;/th&gt;
 &lt;th&gt;支払科目&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;現金&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;Assets:Cash:Purse&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;モバイルSuica&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;Assets:ICCard:MobileSuica&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ビックカメラVIEW&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;Liabilities:CreditCard:BiccameraVIEWSuicaCard&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;補助ツールは&lt;code&gt;.pdf&lt;/code&gt;の直前の要素を対応表で変換する。店舗名の一部から支払方法を推測することはない。&lt;/p&gt;
&lt;p&gt;ファイル名から判別できれば、支払方法の質問は省略する。未記載、未知の表記、PDFの内容との明確な不一致がある場合は質問する。仕訳案の最終確認は、支払方法が分かっていても省略しない。&lt;/p&gt;
&lt;h2 id="pdfの数と取引の数を分ける"&gt;PDFの数と取引の数を分ける
&lt;/h2&gt;&lt;p&gt;領収書とカードの利用票は、同じ購入を示す別の証跡である。これらを別取引として登録すると、費用を二重計上してしまう。&lt;/p&gt;
&lt;p&gt;同じPDFの再取込みは、ファイルごとのSHA-256で検出する。ハッシュを&lt;code&gt;document&lt;/code&gt;のメタデータへ保存し、取込み済みの証跡と照合する。別のPDFが同じ購入を示している場合は、取引情報でも確認する。&lt;/p&gt;
&lt;p&gt;注文番号や伝票番号を優先し、店舗、日付、金額も使って同じ取引の証跡をまとめる。複数PDFの組合せはCodexが候補を提示し、人間が確認する。1つのPDFに領収書と利用票の2ページがある場合も、1取引として扱う。&lt;/p&gt;
&lt;p&gt;仕訳と証跡はBeancountのリンクで紐づける。次は今回登録した給油の例で、店舗名とPDF名は説明用に置き換えている。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-beancount" data-lang="beancount"&gt;2026-10-08 * &amp;#34;ガソリンスタンド&amp;#34; &amp;#34;ハイオク給油&amp;#34; ^receipt-example
 Expenses:Car:Gasoline 2696 JPY
 Expenses:Tax:Consumption-Tax 270 JPY
 Liabilities:CreditCard:BiccameraVIEWSuicaCard -2966 JPY

2026-10-08 document Expenses:Car:Gasoline &amp;#34;../Expenses/Car/Gasoline/receipt.pdf&amp;#34; ^receipt-example
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;既存取引と日付、店舗、費用総額が一致した場合も確認を求める。後から別の証跡を受け取った場合は、既存取引に証跡だけを追加できる。&lt;/p&gt;
&lt;p&gt;PDFは費用科目に応じたフォルダへ移動する。複数科目がある場合は、税を除いて費用額が最大の科目を保管先にする。複数取引で共有するPDFは1回だけ保存し、関連する全取引の登録が終わるまで元ファイルを残す。&lt;/p&gt;
&lt;h2 id="1取引ずつbean-checkを通す"&gt;1取引ずつbean-checkを通す
&lt;/h2&gt;&lt;p&gt;補助ツールは、Codexが作ったJSON形式の仕訳案を受け取り、次の順序で処理する。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;既存の帳簿にエラーがないことを確認する。&lt;/li&gt;
&lt;li&gt;今回の1取引を一時帳簿で検証する。&lt;/li&gt;
&lt;li&gt;PDFを保管先へコピーし、ハッシュを確認する。&lt;/li&gt;
&lt;li&gt;その取引だけを本帳簿へ書き込む。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bean-check main.beancount&lt;/code&gt;を実行する。&lt;/li&gt;
&lt;li&gt;成功したら処理済みとし、不要になった元PDFを削除する。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通常のコピー、書込み、検証に失敗した場合は、今回の帳簿変更を戻して元PDFを残す。失敗した取引を飛ばして後続の処理を進めない。&lt;/p&gt;
&lt;p&gt;専用ツールの&lt;code&gt;check&lt;/code&gt;は事前検証、&lt;code&gt;commit&lt;/code&gt;は1取引の登録を行う。ここでの&lt;code&gt;commit&lt;/code&gt;はGitのコミットではない。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/receipt.py check proposal.json --transaction receipt-example
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python3 scripts/receipt.py commit proposal.json --transaction receipt-example
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;今回作成したツール専用のコマンドなので、Beancountをインストールするだけでは使えない。この記事は実装と運用の記録であり、ツールの配布手順は扱っていない。&lt;/p&gt;
&lt;p&gt;最初にPDFを1件登録し、続いて5件を登録した。後者では領収書と利用票を含む全8ページを確認し、各取引の登録直後に検証が成功した。証跡の移動後はハッシュの一致も確認できた。&lt;/p&gt;
&lt;p&gt;補助ツールの13件のテストも通った。対象には、複数証跡、重複候補、未確認の仕訳、ファイル名の支払方法、コピー失敗、検証失敗後の停止と再開が含まれる。&lt;/p&gt;
&lt;h2 id="残高照合で未記帳のチャージを見つけた"&gt;残高照合で未記帳のチャージを見つけた
&lt;/h2&gt;&lt;p&gt;レシートのない鉄道、飲料、Suicaチャージも、日付、金額、支払方法を伝えて登録した。その後、自分が確認したモバイルSuicaの実残高を帳簿に追記した。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-beancount" data-lang="beancount"&gt;2026-10-11 balance Assets:ICCard:MobileSuica 11362 JPY
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;この時点で&lt;code&gt;bean-check&lt;/code&gt;は失敗した。識別に必要なメッセージを抜粋すると、次の内容だった。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Balance failed for &amp;#39;Assets:ICCard:MobileSuica&amp;#39;: expected 11362 JPY != accumulated 1362 JPY (10000 too little)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;帳簿が実残高より10,000円少なかった。自分が10月5日のチャージを思い出して伝えると、Codexがその日の帳簿にチャージがないことを確認した。ビックカメラVIEW Suicaカードからの10,000円チャージを追加したところ、残高照合を含めて検証が成功した。&lt;/p&gt;
&lt;p&gt;仕訳の検証が通っていても、取引の漏れまでは見つからない。実残高を&lt;code&gt;balance&lt;/code&gt;で照合したことで、未記帳を発見できた。&lt;/p&gt;
&lt;p&gt;現状の対象は2026年の日本円による通常の購入で、外貨や返品は扱っていない。また、突然のプロセス終了や電源断に対して、帳簿とPDFの更新を完全に一体化する保証はない。今回確認できたのは、通常の失敗時の巻き戻しと、対話で確認した仕訳を順番に登録する運用である。&lt;/p&gt;</description></item><item><title>サスペンド復帰後にNextDNSが応答しなくなる問題を調べ、自動再起動を設定した</title><link>https://sonohen.net/posts/2026/10/11/</link><pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate><guid>https://sonohen.net/posts/2026/10/11/</guid><description>&lt;p&gt;Ubuntuの端末をサスペンドから復帰させると、NextDNSから応答を得られなくなることがあった。そのたびに自分でNextDNSを手動再起動していた。毎回操作するのは面倒なので、Codexに症状の調査と自動再起動の実装を依頼した。&lt;/p&gt;
&lt;p&gt;NetworkManagerの接続イベントを受け、約5秒後に稼働中のNextDNSを再起動する仕組みを設定した。内部原因は未特定で、自動再起動による復旧効果は今後確認する。&lt;/p&gt;
&lt;h2 id="調査した環境"&gt;調査した環境
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;項目&lt;/th&gt;
 &lt;th&gt;バージョン&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Ubuntu&lt;/td&gt;
 &lt;td&gt;26.04.1 LTS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Linux Kernel&lt;/td&gt;
 &lt;td&gt;7.0.0-38-generic&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;NextDNS CLI&lt;/td&gt;
 &lt;td&gt;1.47.3&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;NetworkManager&lt;/td&gt;
 &lt;td&gt;1.54.3-2ubuntu3.1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;systemd&lt;/td&gt;
 &lt;td&gt;259.5-0ubuntu3.4&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;以下の実装は、NetworkManagerで接続を管理し、NextDNS CLIがsystemdの&lt;code&gt;nextdns.service&lt;/code&gt;として動作する環境を前提とする。&lt;/p&gt;
&lt;h2 id="wi-fiが戻ってもdns問い合わせは失敗していた"&gt;Wi-Fiが戻ってもDNS問い合わせは失敗していた
&lt;/h2&gt;&lt;p&gt;Codexにログ調査を依頼した。10月11日のホストのjournalでは、次の時系列を確認できた。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;journalctl -b -u nextdns -u systemd-suspend -u NetworkManager &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --since &lt;span style="color:#e6db74"&gt;&amp;#39;2026-10-11 09:05:00&amp;#39;&lt;/span&gt; --until &lt;span style="color:#e6db74"&gt;&amp;#39;2026-10-11 09:35:00&amp;#39;&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;時刻&lt;/th&gt;
 &lt;th&gt;ログで確認した内容&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;09:17:40&lt;/td&gt;
 &lt;td&gt;サスペンドを開始し、Wi-Fiを切断&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:31:19&lt;/td&gt;
 &lt;td&gt;サスペンドから復帰&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:31:25&lt;/td&gt;
 &lt;td&gt;NetworkManagerが&lt;code&gt;CONNECTED_GLOBAL&lt;/code&gt;を報告&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:32:24以降&lt;/td&gt;
 &lt;td&gt;NextDNSのエンドポイント取得がタイムアウト&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:34:04&lt;/td&gt;
 &lt;td&gt;DNS問い合わせのタイムアウトが多数発生&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:34:13&lt;/td&gt;
 &lt;td&gt;自分でNextDNSを再起動&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;09:34:18&lt;/td&gt;
 &lt;td&gt;新しいプロセスがDoH接続を再確立&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;名前解決のエラーには、次の文字列が含まれていた。ログ全文ではなく、識別に必要なメッセージ部分を抜粋している。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;doh resolve: context deadline exceeded
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;エンドポイント取得でも、次のエラーが出ていた。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;exchange: roundtrip: context deadline exceeded
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wi-Fiの接続復旧が報告された後も、同じNextDNSプロセスでDNS問い合わせの失敗が続いていた。この事例では、サービスが落ちたというより、動いたまま上流への通信を回復できていないように見える。&lt;/p&gt;
&lt;p&gt;内部原因は未特定で、NetworkManagerの接続状態だけでは外部との通信が正常とは言い切れない。手動再起動後のログではDoH接続の再確立が記録されており、自分でもDNSの実応答を確認した。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://github.com/nextdns/nextdns/issues/699" target="_blank" rel="noopener"
 &gt;NextDNSの公式リポジトリにあるIssue #699&lt;/a&gt;にも、サスペンド復帰後のDNS問い合わせのタイムアウトが報告されている。こちらはNextDNS 1.37.11とUbuntu 22.04の事例だ。今回と同じ原因とは限らない。&lt;/p&gt;
&lt;h2 id="接続イベントから再起動する"&gt;接続イベントから再起動する
&lt;/h2&gt;&lt;p&gt;プロセスが生きたまま応答できなくなる症状には、終了時の再起動を設定するだけでは対応できない。Codexはこの症状に対応するため、NetworkManagerの接続イベントからNextDNSを再起動する構成を実装した。&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://networkmanager.dev/docs/api/latest/NetworkManager-dispatcher.html" target="_blank" rel="noopener"
 &gt;NetworkManagerのdispatcher仕様&lt;/a&gt;では、&lt;code&gt;up&lt;/code&gt;はインターフェースの接続完了、&lt;code&gt;vpn-up&lt;/code&gt;はVPNの接続完了を表す。Codexの実装では、この2つを対象にしている。&lt;/p&gt;
&lt;p&gt;処理は次の順序で進む。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;dispatcherが&lt;code&gt;up&lt;/code&gt;または&lt;code&gt;vpn-up&lt;/code&gt;を受け取る。&lt;/li&gt;
&lt;li&gt;NextDNSが稼働中なら、systemd timerを再始動する。&lt;/li&gt;
&lt;li&gt;timerが約5秒後にサービスを呼び出す。&lt;/li&gt;
&lt;li&gt;稼働中のNextDNSを再起動する。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;実装には、接続直後の状態が落ち着くまでの猶予として5秒の待ち時間を設けている。外部との通信が復旧したことを確認する処理ではない。&lt;/p&gt;
&lt;p&gt;タイマーの待ち時間中に別の接続イベントを受け取ると、待ち時間を設定し直す。そのため、接続イベントが短時間に続いても、最後のイベントから約5秒後にNextDNSを1回だけ再起動する。通常のWi-Fi再接続やVPN接続でも動作するため、サスペンド復帰だけに限定した仕組みではない。再起動時には短時間DNSが途切れる。&lt;/p&gt;
&lt;h2 id="dispatchertimerserviceの内容"&gt;dispatcher、timer、serviceの内容
&lt;/h2&gt;&lt;h3 id="接続イベントを受け取るスクリプト"&gt;接続イベントを受け取るスクリプト
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;/etc/NetworkManager/dispatcher.d/90-nextdns-reconnect&lt;/code&gt;に配置する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# NetworkManager dispatcher: coalesce connection activation events.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PATH&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/sbin:/usr/bin:/sbin:/bin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export PATH
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;2&lt;span style="color:#66d9ef"&gt;:-&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; up|vpn-up&lt;span style="color:#f92672"&gt;)&lt;/span&gt; ;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; *&lt;span style="color:#f92672"&gt;)&lt;/span&gt; exit &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; ;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Do not undo an intentional stop of NextDNS.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl is-active --quiet nextdns.service &lt;span style="color:#f92672"&gt;||&lt;/span&gt; exit &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;exec systemctl --no-block restart nextdns-reconnect.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--no-block&lt;/code&gt;でタイマーの再始動を依頼し、dispatcher内では待たない。&lt;code&gt;dns-change&lt;/code&gt;には反応しないため、NextDNSの再起動に伴うDNS設定変更が次の再起動を誘発するループを避けられる。&lt;/p&gt;
&lt;h3 id="5秒後に処理するタイマー"&gt;5秒後に処理するタイマー
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;/etc/systemd/system/nextdns-reconnect.timer&lt;/code&gt;に配置する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Delay NextDNS restart after network activation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;OnActiveSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;5s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AccuracySec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;1s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Unit&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;nextdns-reconnect.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="nextdnsを再起動するサービス"&gt;NextDNSを再起動するサービス
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;/etc/systemd/system/nextdns-reconnect.service&lt;/code&gt;に配置する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Restart running NextDNS after network activation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/usr/bin/systemctl try-restart nextdns.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;TimeoutStartSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;45s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Codexは&lt;code&gt;try-restart&lt;/code&gt;を使い、管理者が停止しているNextDNSを勝手に起動しないようにした。&lt;/p&gt;
&lt;h2 id="実機へのインストール"&gt;実機へのインストール
&lt;/h2&gt;&lt;p&gt;Codexはインストール用の&lt;a class="link" href="files/install.sh" &gt;install.sh&lt;/a&gt;も作成した。既存の同名ファイルがあれば停止し、ユニットの検証後に設定を配置する。&lt;/p&gt;
&lt;p&gt;同じディレクトリに次の5つを保存すると、インストール・アンインストールのスクリプトを利用できる。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="files/90-nextdns-reconnect" &gt;90-nextdns-reconnect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="files/nextdns-reconnect.timer" &gt;nextdns-reconnect.timer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="files/nextdns-reconnect.service" &gt;nextdns-reconnect.service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="files/install.sh" &gt;install.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="files/uninstall.sh" &gt;uninstall.sh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保存したディレクトリで、ホストの端末から実行する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo sh ./install.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;dispatcherはroot所有で実行可能にし、グループやほかのユーザーから書き込めない権限にする。インストールスクリプトでは&lt;code&gt;0755&lt;/code&gt;、ユニットファイルは&lt;code&gt;0644&lt;/code&gt;に設定している。&lt;/p&gt;
&lt;p&gt;NetworkManager自体の再起動やタイマーの&lt;code&gt;enable&lt;/code&gt;は不要だ。インストール直後にNextDNSを再起動せず、次の接続イベントから動作する。&lt;/p&gt;
&lt;p&gt;Codex環境からはsudo認証できなかったため、自分でホストの別ターミナルからインストールした。&lt;/p&gt;
&lt;h2 id="検証状況と復帰後の確認"&gt;検証状況と復帰後の確認
&lt;/h2&gt;&lt;p&gt;Codexは&lt;code&gt;sh -n&lt;/code&gt;でシェルの構文を確認し、イベント分岐をモックで検証した。&lt;code&gt;up&lt;/code&gt;と&lt;code&gt;vpn-up&lt;/code&gt;では稼働中の場合だけタイマーを操作し、停止中や対象外のイベントでは操作しないことを確認した。&lt;/p&gt;
&lt;p&gt;Codexの隔離環境ではsystem busへの接続やソケット設定が制限され、現在のサービス状態の取得と&lt;code&gt;systemd-analyze verify&lt;/code&gt;は失敗した。モック検証は、実際のsystemdやサスペンド復帰を使った検証ではない。実機へのインストールは済んだ。手動再起動後のDNSの実応答は自分で確認したが、接続イベントによる自動再起動と、その後の名前解決の回復は未確認だ。&lt;/p&gt;
&lt;p&gt;次回のサスペンド復帰後には、自分で次のコマンドを実行し、ログと名前解決を確認する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;journalctl -b -u nextdns-reconnect.service -u nextdns.service --since &lt;span style="color:#e6db74"&gt;&amp;#39;-10 min&amp;#39;&lt;/span&gt; --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dig @127.0.0.1 example.org +time&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; +tries&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;getent ahosts example.org
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;dig&lt;/code&gt;の例は、NextDNSが&lt;code&gt;127.0.0.1:53&lt;/code&gt;で待ち受ける場合の直接問い合わせだ。待受先が異なる場合は、アドレスとポートを設定に合わせる。&lt;code&gt;getent&lt;/code&gt;ではOSの通常の名前解決を確認する。&lt;/p&gt;
&lt;p&gt;確認したいのは、接続後に自動再起動したログが残り、名前解決も成功することだ。サービスが&lt;code&gt;active&lt;/code&gt;というだけでは、今回の症状が解消したとは判断しない。&lt;/p&gt;
&lt;h2 id="アンインストール"&gt;アンインストール
&lt;/h2&gt;&lt;p&gt;仕組みをアンインストールするときは、ファイルを保存したディレクトリから次を実行する。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo sh ./uninstall.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;アンインストールスクリプトは配置済みファイルと元の実装を比較し、変更されている場合は削除しない。NextDNS本体の設定は変更しない。&lt;/p&gt;</description></item><item><title>EmacsでCodexを使う環境を整え、関連書籍を買った日</title><link>https://sonohen.net/posts/2026/10/10/</link><pubDate>Sat, 10 Oct 2026 00:00:00 +0000</pubDate><guid>https://sonohen.net/posts/2026/10/10/</guid><description>&lt;p&gt;今日は、EmacsからCodexを使うための環境を整えていた。CLIのインストールから始めたのだが、ターミナルと日本語入力、Docker、権限の設定まで、思ったより作業が増えた。&lt;/p&gt;
&lt;h2 id="codexをインストールする"&gt;Codexをインストールする
&lt;/h2&gt;&lt;p&gt;まず、Codexをコマンドラインから使えるようにした。今日の環境では、次のコマンドでインストールしている。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo npm install -g @openai/codex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;which codex
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;which codex&lt;/code&gt;の結果は&lt;code&gt;/usr/local/bin/codex&lt;/code&gt;だった。&lt;/p&gt;
&lt;h2 id="emacsではvtermを使うことにした"&gt;Emacsではvtermを使うことにした
&lt;/h2&gt;&lt;p&gt;Codexをインストールしたものの、Emacsのシェルではうまく動かなかった。そこで、vtermを使うためのパッケージを入れた。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install -y cmake libvterm-dev libtool-bin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ところが、今回の環境ではvterm上でDDSKKによる日本語入力ができなかった。別のターミナルならどうかと思い、Eatも試してみた。&lt;/p&gt;
&lt;p&gt;NonGNU ELPAをパッケージの取得先に追加し、leafで次のように設定した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(leaf eat
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :ensure &lt;span style="color:#66d9ef"&gt;t&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :bind (&lt;span style="color:#e6db74"&gt;&amp;#34;C-c v&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;.&lt;/span&gt; eat-project-other-window))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Eatでも日本語入力ができず、動作も安定しなかったため、今回はvtermを使うことにした。DDSKKで入力するという当初の希望は、ひとまず保留にした。これは今日の自分の環境での結果であり、ほかの環境でも同じとは限らない。&lt;/p&gt;
&lt;h2 id="codex用のdocker環境を作る"&gt;Codex用のDocker環境を作る
&lt;/h2&gt;&lt;p&gt;次に、CodexをDockerで動かすための環境を作った。Dockerの導入は&lt;a class="link" href="https://docs.docker.com/engine/install/ubuntu/" target="_blank" rel="noopener"
 &gt;公式のUbuntu向けインストール手順&lt;/a&gt;を参考にした。&lt;/p&gt;
&lt;p&gt;インストール後、次のコマンドで確認した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run hello-world
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Hello from Docker!&lt;/code&gt;と表示されたので、続いてCodex用のDockerfileと起動スクリプトを用意した。&lt;/p&gt;
&lt;p&gt;当日の作業メモに残した初期構成では、イメージのベースに&lt;code&gt;node:22-bookworm-slim&lt;/code&gt;を使い、Git、ripgrep、PythonなどとCodex CLIをインストールする。コンテナ内では一般ユーザーの&lt;code&gt;node&lt;/code&gt;で実行し、作業ディレクトリを&lt;code&gt;/workspace&lt;/code&gt;にした。&lt;/p&gt;
&lt;p&gt;起動スクリプトでは、ホスト側の共有ディレクトリを&lt;code&gt;/workspace&lt;/code&gt;にマウントし、Codexのホームには名前付きボリュームを使った。当日のメモに残した初期のスクリプトを以下に示す。Dockerfileと起動スクリプトはその後も変更しているため、以下は初期構成の記録として読む。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;#!/bin/sh
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;set -eu
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shared_dir&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/home/sonohen/dockershared
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir -p &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$shared_dir&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;exec docker run --rm -it --init &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -e TERM &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -v codex-home:/home/node/.codex &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --mount &lt;span style="color:#e6db74"&gt;&amp;#34;type=bind,src=&lt;/span&gt;$shared_dir&lt;span style="color:#e6db74"&gt;,dst=/workspace&amp;#34;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; codex-cli &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$@&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;shared_dir&lt;/code&gt;は自分の環境のパスなので、別の環境で使う場合は変更する。コンテナから共有ディレクトリ内のファイルは変更できるため、何を共有するかは意識しておきたい。&lt;/p&gt;
&lt;p&gt;イメージの作成と起動には、次のコマンドを使った。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker build -t codex-cli -f Dockerfile .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;./run-codex.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="bubblewrapとapparmorの設定で悩む"&gt;bubblewrapとAppArmorの設定で悩む
&lt;/h2&gt;&lt;p&gt;環境を整える途中で、bubblewrapが新しいnamespaceを作成できないというメッセージにも遭遇した。作業メモに残した原文は次のとおりだ。単語間の空白も記録どおりに示す。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bwrap:No permissions tocreate a new namespace,likely because the kerneldoes not allow non-privileged user namespaces.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;メッセージには、非特権ユーザーによるnamespaceの作成がカーネルで許可されていない可能性が示されていた。そこで、UbuntuのAppArmor設定を調べた。&lt;/p&gt;
&lt;p&gt;まず、次の設定値を確認した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sysctl kernel.apparmor_restrict_unprivileged_userns
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;結果は次のとおりだった。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kernel.apparmor_restrict_unprivileged_userns = 1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この値を確認したうえで、&lt;code&gt;/usr/bin/bwrap&lt;/code&gt;に&lt;code&gt;userns&lt;/code&gt;を許可するAppArmorプロファイルを作った。管理者権限で次のコマンドを実行し、&lt;code&gt;apparmor_parser&lt;/code&gt;で読み込んだ。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo install -m &lt;span style="color:#ae81ff"&gt;644&lt;/span&gt; /dev/stdin /etc/apparmor.d/bwrap &lt;span style="color:#e6db74"&gt;&amp;lt;&amp;lt;&amp;#39;EOF&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;abi &amp;lt;abi/4.0&amp;gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;include &amp;lt;tunables/global&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;profile bwrap /usr/bin/bwrap flags=(unconfined) {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; userns,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; include if exists &amp;lt;local/bwrap&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;EOF&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apparmor_parser -r /etc/apparmor.d/bwrap
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この設定後、私が使っている範囲ではbubblewrap関連のエラーは出なくなった。すべての実行条件を検証したわけではないため、今回確認できた範囲での結果として記録しておく。&lt;/p&gt;
&lt;p&gt;また、Codexの&lt;code&gt;config.toml&lt;/code&gt;には次の設定を追加した。これは今回の環境で追加した内容の記録なので、末尾のパスはインストール先に合わせる必要がある。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;default_permissions&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;project-edit&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;permissions&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;project-edit&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;filesystem&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;:minimal&amp;#34;&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/usr/local/lib/node_modules/@openai/codex&amp;#34;&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a class="link" href="https://learn.chatgpt.com/docs/sandboxing?surface=app#app-prerequisites" target="_blank" rel="noopener"
 &gt;OpenAIのサンドボックスの説明&lt;/a&gt;にも、Linuxで必要になるbubblewrapと、UbuntuでのAppArmor設定について記載がある。Ubuntuのバージョンによって扱いが異なるため、同じメッセージが出た場合も、自分の環境に合う手順を確認したい。&lt;/p&gt;
&lt;h2 id="ubuntuで行ったほかの作業"&gt;Ubuntuで行ったほかの作業
&lt;/h2&gt;&lt;h3 id="aptの段階的な更新を知った"&gt;aptの段階的な更新を知った
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;sudo apt upgrade&lt;/code&gt;を実行したところ、&lt;code&gt;Not upgrading yet due to phasing&lt;/code&gt;と表示され、20個のパッケージが更新されなかった。&lt;/p&gt;
&lt;p&gt;作業メモから、表示の冒頭と更新件数を抜粋する。途中のパッケージ一覧は省略した。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Not upgrading yet due to phasing:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;（パッケージ一覧を省略）
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Summary:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 20
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;調べると、Ubuntuには更新を一度に全員へ配布せず、対象を段階的に広げる仕組みがある。&lt;a class="link" href="https://ubuntu.com/project/docs/how-ubuntu-is-made/concepts/phased-updates/" target="_blank" rel="noopener"
 &gt;Phased updatesの公式説明&lt;/a&gt;によると、不具合が見つかった際の影響を小さくするためのものだ。今回の表示は、その段階的な配布に関係していた。&lt;/p&gt;
&lt;h3 id="localsearchを一時的に止めた"&gt;localsearchを一時的に止めた
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;localsearch-extractor-3&lt;/code&gt;の動作が気になったので、ひとまず次のコマンドでサービスを停止し、起動を抑止した。原因はまだ分かっていない。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user mask --now localsearch-3.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;実行すると、すぐにプロセスが停止した。元に戻すときのコマンドも記録しておく。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user unmask localsearch-3.service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user start localsearch-3.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="ibus-skkを入れた"&gt;ibus-skkを入れた
&lt;/h3&gt;&lt;p&gt;vtermではDDSKKによる日本語入力ができなかったので、ibus-skkと辞書もインストールした。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install ibus-skk skkdic
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ところが、vtermとの組み合わせでは変換した文字が消えてしまうなどの問題があり、ibus-skkは使わないことにした。今回の環境では、vterm上の日本語入力はUbuntuのMozcに頼るしかなさそうだ。&lt;/p&gt;
&lt;h2 id="codexの本を買った"&gt;Codexの本を買った
&lt;/h2&gt;&lt;p&gt;本屋で『Codexではじめるエージェンティックコーディング』を見かけ、タイトルが気になったので購入した。&lt;/p&gt;
&lt;p&gt;初版ということもあり、少し誤植が気になる。今後の修正に期待しつつ、読書メモを残しているところだ。&lt;/p&gt;
&lt;h3 id="指示に目標と完了条件を書く"&gt;指示に目標と完了条件を書く
&lt;/h3&gt;&lt;p&gt;読書メモには、Mini Codexテンプレートとして、指示に含める4つの項目を整理した。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Goal：達成したい目標を書く。&lt;/li&gt;
&lt;li&gt;Context Pointers：既存の実装や仕様など、参照してほしい情報の場所を示す。&lt;/li&gt;
&lt;li&gt;Constraints：変更してはいけない仕様や、守るべき規約を書く。&lt;/li&gt;
&lt;li&gt;Done When：作業が完了したと判断する条件を書く。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;AIに必要な情報を渡さなければ、推論や一般的な情報で補われてしまう。自分の作業に必要な文脈を、指示の中に具体的に含めることが大切だと受け取った。&lt;/p&gt;
&lt;p&gt;特に完了条件は、実行や確認によって判定できる形にしたい。「きれいなコードにする」よりも、「lintエラーが0件」「既存テストがすべてpassする」のように書く。&lt;/p&gt;
&lt;h3 id="plan-modeを使う場面を考える"&gt;Plan modeを使う場面を考える
&lt;/h3&gt;&lt;p&gt;Plan modeについては、方向性は決まっていても、詳細を相談しないと完成形が決まらない作業に向いているとメモした。タイプミスの修正や、単一ファイルの文言変更なら、計画を作る手間との釣り合いを考えたい。&lt;/p&gt;
&lt;p&gt;計画を確認するときは、目標、参照情報、制約、完了条件に加え、変更の範囲と順序も見る。変更が広がりすぎていないか、戻しやすい単位になっているかを確認する。&lt;/p&gt;
&lt;p&gt;また、調査やテスト、小さな変更を先に置き、影響の大きい変更を後にできるかも考える。タスクの分け方を判断するときには、複数の観点で検証できるかを確認したい。&lt;/p&gt;
&lt;p&gt;今日はCodexをEmacsから使う準備に多くの時間を使った。vtermを使う方針が決まり、使っている範囲ではbubblewrapのエラーも出なくなった。日本語入力はDDSKKとibus-skkを試したものの、Mozcに頼る方向で考えている。次は、用意した環境で実際の作業を続けながら使い勝手を見ていきたい。&lt;/p&gt;</description></item></channel></rss>