ローカルでエージェントを動かしているうちは、資格情報の話はあまり表に出てきません。自分の端末の環境変数に API キーが入っていて、エージェントはそれを読むだけ。鍵がどこにあるかは自分が把握していて、端末を閉じれば実行も止まります。
Managed Agents API でクラウド側にタスクを逃がした最初の日、私はこの安心が手元にしかなかったことに気づきました。クラウドで走るエージェントに、これまでと同じ感覚で本番デプロイ用のトークンを環境変数に流し込もうとして、手が止まったのです。
そのトークンは、自分の端末を離れて、自分の見えない場所で、自分が見ていない時間に使われます。もし途中で何かが漏れたら、被害が及ぶ範囲も、続く時間も、自分ではすぐに止められません。
クラウドにエージェントを逃がすことの本質は「実行が手元を離れる」ことです。だとすれば、資格情報も「手元を離れても安全な形」に作り替える必要があります。
長命トークンが手元を離れる怖さ
普段使っている資格情報の多くは、長く生きるように作られています。一度発行したら、明示的に取り消すまで有効。ローカルで自分だけが使う前提なら、これで困りません。
問題は、この性質がクラウド実行と相性が悪いことです。
長命トークンには三つの弱点があります。まず、漏れたときに被害が止まりません。失効させるまで何度でも使えるので、気づくのが遅れれば遅れるほど傷が広がります。次に、権限が広すぎます。「とりあえず動くように」と強い鍵を渡しがちで、エージェントが本来触る必要のないリソースまで届いてしまいます。そして、足跡が追えません。同じトークンを複数の実行で使い回すと、どの実行が何をしたのかが後から切り分けられなくなります。
ローカルではこの三つがほとんど顕在化しません。被害範囲は自分の端末に閉じ、権限が広くても自分の操作の延長で、足跡は自分の記憶で補える。けれどクラウドでは、この三つがそのまま運用上のリスクになります。
ですから設計の方針は単純です。長く生きる強い鍵は手元に置いたまま、クラウドへ渡すのは「短く生きて、狭くしか効かない」別の資格情報にする。これに尽きます。
実行ごとに発行し、終わったら捨てる
第一の柱は、トークンの寿命を実行の寿命に合わせることです。
エージェントを起動する前に、その実行のためだけのトークンを発行します。タスクが終われば、成功でも失敗でも、そのトークンを失効させます。次の実行はまた新しいトークンを受け取ります。こうしておけば、たとえ実行中にトークンが漏れても、有効なのはその実行が終わるまでの短い時間だけです。
仲介役を一枚挟む形になります。手元の強い鍵を持つのは仲介層だけで、エージェントには仲介層が発行した短命トークンしか渡しません。
# token_broker.py — 実行ごとに短命トークンを発行・失効する仲介層
import time
import secrets
from dataclasses import dataclass, field
@dataclass
class LeasedToken:
value: str
scope: tuple[str, ...] # この実行で許す操作だけ
expires_at: float
run_id: str
class TokenBroker:
"""強い鍵は内側に隠し、外へは短命・狭権限のトークンだけを貸し出す。"""
def __init__(self, master_key: str, default_ttl: int = 600):
self._master_key = master_key # 手元から出さない
self._default_ttl = default_ttl
self._active: dict[str, LeasedToken] = {}
def issue(self, run_id: str, scope: tuple[str, ...], ttl: int | None = None) -> LeasedToken:
ttl = ttl or self._default_ttl
token = LeasedToken(
value=f"agt_{secrets.token_urlsafe(24)}",
scope=scope,
expires_at=time.time() + ttl,
run_id=run_id,
)
self._active[token.value] = token
return token
def authorize(self, token_value: str, action: str) -> bool:
token = self._active.get(token_value)
if token is None:
return False
if time.time() > token.expires_at:
self.revoke(token_value) # 期限切れは即座に無効化
return False
return action in token.scope # 許した操作以外は拒否
def revoke(self, token_value: str) -> None:
self._active.pop(token_value, None)
# 使い方
broker = TokenBroker(master_key="本番の強い鍵はここに隠す")
def run_agent_task(run_id: str):
# この実行に必要な操作だけを渡す
token = broker.issue(run_id, scope=("read:articles", "write:draft"), ttl=300)
try:
dispatch_to_cloud_agent(run_id, token.value) # エージェントへは短命トークンだけ
finally:
broker.revoke(token.value) # 成否を問わず必ず失効要点は finally で必ず revoke していることです。例外で落ちようが、正常に終わろうが、トークンは実行の終わりで死にます。ttl を短く切ってあるのは二重の保険で、もし失効処理自体に到達できなくても、時間切れで自動的に効かなくなります。
エージェント側のコードは、自分が受け取ったトークンが短命であることを意識する必要すらありません。普通のトークンとして使い、実行が終われば自然に無効になる。複雑さは仲介層に閉じ込められています。