デスクトップの Antigravity では一度ログインすれば数日間プロンプトが出ない。ところが同じアカウントで組んだサーバー側のスケジュール実行だけ、数時間おきに認証が切れて空振りする——この非対称に、しばらく首をかしげていました。
原因は権限でもネットワークでもなく、トークンをどこに置くか でした。v2.2.1 で、更新後の OAuth トークンは OS のキーリングへ自動保存されるようになっています。デスクトップではこれが効いて再認証が激減します。けれど端末も画面もないヘッドレスのサーバーには、その「キーリング」という保管庫がそもそも立ち上がっていないことがあります。保存しようとして黙って失敗し、次の起動ではトークンが見つからず、また最初から取り直す。この繰り返しが「数時間おきに切れる」の正体でした。
個人開発で Dolice Labs の複数サイトを無人で回している私にとって、この手の「デスクトップでは再現しない不具合」が一番やっかいです。手元では起きないので、ログを保管場所の視点で読み直すまで見当がつきませんでした。同じ静かな失敗に手を焼く方が、保管庫という視点を最初から持てるように、仕組みからヘッドレス向けの設計、そして本番運用での落とし穴の回避まで順にたどっていきます。
OS キーリングという保管庫の正体
まず、「キーリング」が具体的に何を指すのかを OS ごとに揃えておきます。ここが曖昧なままだと、対処が当て推量になります。
OS 保管庫の実体 アクセスの前提
macOS Keychain(login キーチェーン) ログインセッションが解錠されていること
Windows Credential Locker(資格情報マネージャー) 対話ユーザーのログオンセッション
Linux(デスクトップ) Secret Service(gnome-keyring / KWallet) D-Bus セッションと解錠済みの keyring デーモン
Linux(ヘッドレス) 多くの場合、存在しない D-Bus セッションが立たず Secret Service に応答者がいない
要点は、macOS と Windows は「対話ログインしたユーザーのセッション」に強く結びついている点です。SSH で入っただけのシェルや、ログイン画面を通らずに起動する常駐サービスからは、同じユーザー名でも解錠済みのキーチェーンに手が届かないことがあります。
Linux のデスクトップでは Secret Service という共通規格があり、gnome-keyring や KWallet がその応答者として動きます。逆に言えば、その応答者がいない環境では、保存要求そのものが宛先を失う ということです。ヘッドレスのサーバーやコンテナがまさにこれに当たります。
ヘッドレスでキーリングが消える瞬間
症状を分解します。無人のスケジュール実行で認証が崩れるとき、内部では次の順に事が起きています。
エージェントがトークンを更新する(ここまでは成功する)
更新後のトークンを OS キーリングへ保存しようとする
キーリング(Secret Service の応答者)が存在せず、保存が失敗する
その失敗が致命傷として扱われず、実行はそのまま進む
次回の起動でキーリングを読みにいくが、当然何もない
トークンが見つからず、再取得か、端末のない環境での再ログイン待ちに落ちる
厄介なのは 4 の「黙って進む」部分です。保存の失敗はしばしば警告どまりで、終了コードには表れません。以前に扱った、Antigravity CLI が無人実行中に 401 で止まるとき の「成功と記録されるのに中身が空」という現象と、根は同じ静かな失敗です。あちらがトークンの寿命の問題なら、こちらはトークンの置き場所 の問題、という違いがあります。
まず、自分の実行環境に保管庫の応答者がいるかを確かめます。
#!/usr/bin/env bash
# 目的: Secret Service(キーリングの応答者)が実際に応答するかを判定する。
# いる/いないを最初に確定させないと、以降の対処が当て推量になる。
set -uo pipefail
has_secret_service () {
# D-Bus セッションが無ければ、その時点で応答者はいない。
[ -n "${ DBUS_SESSION_BUS_ADDRESS :- }" ] || return 1
# org.freedesktop.secrets が bus 上に存在するかを問い合わせる。
command -v dbus-send > /dev/null 2>&1 || return 1
dbus-send --session --dest=org.freedesktop.DBus \
--type=method_call --print-reply \
/org/freedesktop/DBus org.freedesktop.DBus.ListNames 2> /dev/null \
| grep -q "org.freedesktop.secrets"
}
if has_secret_service ; then
echo "[keyring] Secret Service あり: OS キーリングを使えます"
else
echo "[keyring] Secret Service なし: ヘッドレス向けの保管戦略へ切り替えます"
fi
この判定を「本処理の前」に置くのが肝心です。保存が失敗してから気づくのではなく、保管庫の有無を先に確定させてから経路を選ぶ 。そうすることで、後段の分岐が推測ではなく事実に基づきます。
保管バックエンドを明示的に選ぶ
保管庫が「あるかもしれない/ないかもしれない」に依存した自動化は、環境が少し変わるたびに崩れます。ヘッドレス運用では、暗黙の既定に任せず、使う保管バックエンドを明示的に固定するのが安定します。
多くの認証まわりのツールは、環境変数か設定ファイルで保管バックエンドを選べるように作られています。手元の版で選択肢の名前は変わり得るので、antigravity --help や設定ドキュメントで実際のキー名を確認してから、次のように「環境ごとに宣言する」形にします。
#!/usr/bin/env bash
# 目的: 実行環境に応じて保管バックエンドを1つに固定する。
# 「たまたま動く」をやめ、どの経路で保存されるかを常に自分で握る。
set -euo pipefail
CRED_DIR = "${ XDG_STATE_HOME :- $HOME / . local / state }/antigravity"
mkdir -p " $CRED_DIR "
chmod 700 " $CRED_DIR "
if has_secret_service ; then
# デスクトップ相当: OS キーリングに任せる。
export ANTIGRAVITY_CRED_BACKEND = "keyring"
else
# ヘッドレス: 権限を絞ったファイルに固定する。
export ANTIGRAVITY_CRED_BACKEND = "file"
export ANTIGRAVITY_CRED_FILE = " $CRED_DIR /credentials.json"
fi
echo "[keyring] backend=${ ANTIGRAVITY_CRED_BACKEND }"
ここで has_secret_service は前節の関数です。変数名(ANTIGRAVITY_CRED_BACKEND など)はあくまで例で、実際のツールが読む環境変数名に置き換えてください。設計上の意図は変わりません——保存経路を実行時に一意に決め、ログに残す ことです。どこに保存されたか分からないトークンは、後で必ず調査を長引かせます。
ファイルフォールバックを安全に設計する
ヘッドレスでキーリングが使えないとき、素朴にトークンを平文ファイルへ書くと、今度は権限が問題になります。ホームディレクトリ配下でも、既定のマスク次第では同一グループから読めてしまいます。ここは「動く」だけでなく「読めない」ことまで設計します。
#!/usr/bin/env bash
# 目的: ファイルフォールバック時に、書き込みと同時に権限を 0600 へ落とす。
# 「書けた」ことと「他人に読めない」ことは別問題。両方を1つの操作で保証する。
set -euo pipefail
write_credentials () {
local dest = " $1 " payload = " $2 "
local tmp
tmp = "$( mktemp "${ dest }.XXXXXX")" # 同一ディレクトリに一時ファイルを作る
chmod 600 " $tmp " # 中身を書く前に権限を絞る
printf '%s' " $payload " > " $tmp "
mv -f " $tmp " " $dest " # 原子的に差し替える(読み手が半端な状態を見ない)
chmod 600 " $dest "
}
# 検証: 保存直後に、権限が想定どおりかを必ず確かめる。
verify_permissions () {
local f = " $1 "
local mode
mode = "$( stat -c '%a' " $f " 2> /dev/null || stat -f '%Lp' " $f ")
if [ " $mode " != "600" ]; then
echo "[keyring ] 権限が 600 ではありません(現在 ${mode})。中止します。 " >&2
return 1
fi
echo " [keyring] 権限 600 を確認しました "
}
三点、意図を補足します。第一に、権限を落とすのは中身を書く前 です。先に書いてから chmod すると、その一瞬だけ他人に読める窓が開きます。第二に、mktemp と mv -f で原子的に差し替えます。トークンの更新中に別プロセスが読みにきても、半分だけ書かれた壊れた JSON を掴ませないためです。第三に、書いたあと必ず stat で 0600 を確認します。私はこの検証を省いて、umask が緩い CI コンテナでだけファイルがグループ可読になっていたのを、後から冷や汗とともに見つけたことがあります。
平文で置くことに抵抗があれば、保存前にマシン固有の鍵で暗号化する層を挟めます。ただしその鍵をどこに置くのか、という同じ問題が入れ子になるだけなので、まずは 0600 と最小権限のディレクトリ(0700)で守り、そのうえで必要なら暗号化を足す、という順序を私は好みます。鍵そのものの入れ替えについては、無人のエージェントを止めずに鍵を入れ替える で扱った重複期間つきローテーションの考え方が、そのまま応用できます。
Linux のヘッドレスでキーリングを「あえて」立てる選択
ファイルフォールバックが基本線ですが、どうしても Secret Service を使いたい場合——たとえば既存のツール群がキーリング前提で作られている場合——には、ヘッドレスでも応答者を立てる手があります。使うかどうかは別として、選択肢として知っておくと判断に幅が出ます。
#!/usr/bin/env bash
# 目的: ヘッドレス Linux で、その場かぎりの D-Bus セッションと
# 解錠済み gnome-keyring を起動し、そのシェルの中でジョブを走らせる。
# 注意: 解錠パスフレーズの受け渡しが新たな秘密管理の対象になる点は理解しておく。
set -euo pipefail
run_with_keyring () {
local passphrase = " $1 " ; shift
dbus-run-session -- bash -c '
printf "%s" "'" $passphrase "'" \
| gnome-keyring-daemon --unlock --components=secrets >/dev/null 2>&1
exec "$@"
' _ " $@ "
}
# 使い方: パスフレーズは環境変数などから安全に渡す(ハードコードしない)。
# run_with_keyring "$KEYRING_PASSPHRASE" antigravity run my-task
正直に言えば、これは便利さと引き換えに、解錠パスフレーズという新しい秘密 を運用対象に増やします。トークンを守るための仕組みが、別の秘密を生む。この入れ子を割に合うと見るかどうかは、既存資産の量しだいです。まっさらな自動化を新しく組むなら、私はファイルフォールバックの単純さを推奨します。既存のキーリング依存が厚い現場なら、この起動ラッパーで橋を架けるほうが移行の傷が浅くなります。
保存場所ごとに、トークンの生死を確かめる
保管戦略を選んだら、最後に「そこに生きたトークンが本当にあるか」を本処理の前に確かめます。バックエンドが違えば確認方法も違うので、経路ごとに検査を分けます。
#!/usr/bin/env bash
# 目的: 選んだバックエンドに応じて、本処理の前にトークンの存在と鮮度を確かめる。
# 「保存できているつもり」を、実行のたびに事実として検証し直す。
set -uo pipefail
preflight_credentials () {
case "${ ANTIGRAVITY_CRED_BACKEND :- }" in
keyring )
# キーリング経由: ツールの認証状態確認コマンドに委ねる。
antigravity auth status > /dev/null 2>&1
;;
file )
local f = "${ ANTIGRAVITY_CRED_FILE :? }"
[ -s " $f " ] || return 1 # 空でないこと
verify_permissions " $f " || return 1 # 権限が 0600 であること
# 有効期限が読めるなら、残り時間が短すぎないかも見る。
;;
*)
echo "[keyring] backend 未設定。経路を先に確定してください。" >&2
return 2
;;
esac
}
if ! preflight_credentials ; then
echo "[keyring] 認証プリフライト失敗。本処理に入らず終了します。" >&2
exit 78 # EX_CONFIG として明示的に失敗させる
fi
echo "[keyring] 認証プリフライト OK"
この「本処理の直前に軽い検査を1回だけ挟む」型は、認証に限らず無人運用の土台になります。より一般的なプリフライトの組み方は スケジュール実行の直前に Antigravity CLI が本当に応答するかを1回だけ確かめる にまとめてあります。認証チェックはそのプリフライトの一項目として組み込むと、失敗の切り分けがきれいになります。
私の運用では、トークンの有効期限が残り 10 分を切っていたら本処理の前に先回りして更新する、という余白も持たせています。境界ぎりぎりのトークンで長い処理に入ると、途中で切れて 401 に落ちるからです。5 分でも足りることは多いのですが、複数サイトを順に処理する私のジョブでは 1 本が 10 分近くかかることがあり、そこに合わせて余白を決めました。この数字は、自分のジョブの最長実行時間から逆算するのが確実です。
複数マシンでトークンを同期すべきか
最後に、設計判断として一番迷うところに触れます。同じアカウントで 3 台のマシン(手元のデスクトップ、常駐サーバー、CI)を回すとき、トークンを 1 箇所に集めて同期したくなります。けれど私は、同期しない 方針を採っています。
理由は二つあります。ひとつは、同期の仕組み自体が新しい攻撃面と障害点になること。トークンを配って回るパイプラインが壊れれば、全台が同時に沈黙します。もうひとつは、リフレッシュトークンの取り回しです。複数の場所から同じリフレッシュトークンを同時に使うと、片方の更新がもう片方のトークンを無効化し、互いに蹴り合う競合が起きることがあります。
代わりに、マシンごとに独立して認証を持たせ、それぞれが自分の保管庫(デスクトップはキーリング、サーバーと CI はファイル)でトークンを完結させます。台数が増えれば初期ログインの手間は増えますが、1 台の障害が他へ波及しない独立性のほうを、無人運用では優先したいと考えています。この判断は環境によって変わり得るので、あくまで私自身の現在地としてお読みください。
次のアクション
まず、いま無人で動いている実行環境で、この記事の最初のスニペット(has_secret_service)を 1 回だけ走らせてみてください。デスクトップでは「あり」、サーバーでは「なし」と出るはずです。その一行の差が、これまでの「サーバーだけ認証が切れる」の答えになっているかもしれません。そこから、保存経路を明示的に固定する設計へ一歩進められます。
保管場所という視点は、一度持つと認証まわりのログの読み方がはっきり変わります。私自身まだ運用しながら調整を続けている途中ですが、同じ静かな失敗に手を焼いている方の遠回りが少しでも減れば嬉しいです。お読みいただきありがとうございました。