SafeAgent — Execution Guard for AI Agents について
SafeAgentは、操作の再試行による副作用の重複を防ぐ、AIエージェント向けの実行ガードです。永続的なクレームを記録し、結果が未確定の操作は照合されるまで再実行をブロックします。
主な機能
- 01安定した request_id によるクレーム
- 02同時実行と重複操作の抑制
- 03未確定状態での再試行ブロック
- 04操作結果によるクレーム確定
- 05絞り込み可能なクレーム監査履歴
活用シーン
- 01タイムアウト後の二重請求防止
- 02再試行されるメールや Webhook の重複排除
- 03取引やリソース作成の重複防止
- 04クラッシュ後に残った未確定操作の調査
MCP 対応
stdio 経由で safeagent_claim と safeagent_settle を MCP ツールとして公開し、MCP クライアントから操作のクレームと結果確定を行えます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
SafeAgent — AI エージェント向け実行ガード
POST /claim · safeagent-production.up.railway.app
ダッシュボード:safeagent-dashboard-2.vercel.app
# Claim an action — free, no payment header required
curl -s -X POST https://safeagent-production.up.railway.app/claim \
-H "Content-Type: application/json" \
-d '{"request_id":"order:TQQQ:buy:6:2026-05-19T13:31:00-04:00","action":"order"}'
# → {"status":"PROCEED","request_id":"..."}
# Settle after the action fires
curl -s -X POST https://safeagent-production.up.railway.app/settle/order:TQQQ:buy:6:2026-05-19T13:31:00-04:00 \
-H "Content-Type: application/json" \
-d '{"result":{"status":"completed"}}'
# → {"status":"committed","request_id":"..."}
# Retry with the same request_id — returns SKIP
curl -s -X POST https://safeagent-production.up.railway.app/claim \
-H "Content-Type: application/json" \
-d '{"request_id":"order:TQQQ:buy:6:2026-05-19T13:31:00-04:00","action":"order"}'
# → {"status":"SKIP","request_id":"...","existing":{...}}
# Free test endpoint — same logic, limited to 10 calls per IP
curl -s -X POST https://safeagent-production.up.railway.app/claim/test \
-H "Content-Type: application/json" \
-d '{"agent_id":"bot-1","action_type":"order","scope":"TQQQ:buy:bar:2026-05-19T13:31:00-04:00"}'
# → {"status":"PROCEED","request_id":"...","test":true,"calls_remaining":9}
Bazaar にインデックスされています。
CrewAI のリトライを実測した A/B テスト
認証情報を使わない実験で、CrewAI 1.15.21 の実際の同一プロセス内における `ToolUsage._use` のリトライを SafeAgent 0.1.23 に対して検証します。効果発生後・確認前の試行を30回行った結果、ガードなしの経路では論理アクションごとに2回の効果が記録されました。一方、SafeAgent の経路では効果が1回記録され、照合のために `PENDING` のクレームが保持されました。
対象範囲: スクリプトによる決定論的な LLM と、プロセス外にある機能分離された台帳に記録された無害な効果です。この検証では、プロセスの終了、新しいワーカーによる復旧、実際の外部プロバイダー、SafeAgent Control による照合は検証していません。
SafeAgent Control の創設パイロット
外部オペレーター向けに、範囲を限定した読み取り専用の照合パイロットを2件募集しています。SafeAgent Control が、それまで知られていなかった再現可能な不一致を特定し、オペレーターが対応可能と合意した場合を除き、料金は発生しません。
Mycelium Trails
この実装はMycelium Providerです。
完了した各アクションは、コンテンツアドレス方式の action_ref
(4つのプリイメージフィールドに対する JCS+SHA-256)とともに ARGENTUM にトレイルを送信します。返された mycelium_trail_id は
すべてのレシートに含まれ、独立した検証に利用できます。
Soma で検証済み
SafeAgent は、Soma(Mycelium エージェントカタログ)で初めて検証された外部インテグレーターです。本番環境でのすべての実行は Mycelium Trails を通じてオンチェーンに記録され、オペレーターを介さずに独立して検証できます。
- インテグレーターバッジ — Mycelium Trails を本番環境で実行する、検証済みの外部クライアント
- ライブトレイル — https://argentum-api.rgiskard.xyz/dashboard/trails?client=safeagent-prod
- 公開監査 — すべての監査人が SafeAgent の実行を独立して検証できます
できること
SafeAgent は、耐久性のある実行クレームのガードです。同じ安定した request_id を使った同時実行や再試行を抑止し、未解決の試行を、暗黙のうちに再試行するのではなく、照合のために保持します。
すべてのアクションには、論理アクションから導出される安定した request_id が割り当てられます。新しいキーは PROCEED を返し、確定済みのキーは保存されている結果とともに SKIP を返します。タイムアウトやクラッシュ後にキーが PENDING のままの場合、ローカル状態だけではプロバイダーがアクションを受け付けたかどうかを証明できないため、ブロックされたままになります。プロバイダーの証拠を確認するか、プロバイダー固有の冪等性を利用してから復旧してください。
ステートマシン: CLAIMABLE → PENDING → COMMITTED。後続のクレームでは PENDING または SKIP が返されます。
SafeAgent が防止する一般的な障害モード:
| シナリオ | SafeAgent がない場合 | SafeAgent がある場合 |
|---|---|---|
| Stripe の請求が受け付けられた後にタイムアウトする | 再試行により二重請求される可能性があります | 同じキーはプロバイダーとの照合が済むまで PENDING のままです |
| サインアップ時のウェルカムメールが再試行される | ユーザーにウェルカムメールが 2 通届きます | 2 回目の送信は SKIP を返します |
| Webhook が 2 回配信される(Stripe/GitHub/Twilio は少なくとも 1 回の配信を保証) | イベントが 2 回処理されます | 2 回目の処理は SKIP を返します |
| 再試行時にワークスペースがプロビジョニングされる | ワークスペースが 2 つ作成されます | 2 回目のプロビジョニングは SKIP を返します |
| クラッシュ後に AI エージェントのツール呼び出しが再試行される | 重複した副作用が発生します | 同じキーはブロックされたままになり、結果は未解決であることが明示されます |
保証の範囲: SafeAgent が証明するのはローカルのクレーム状態であり、外部での結果ではありません。
COMMITTEDは呼び出し元がレシートを確定したことを意味します。PENDINGは未解決であることを意味し、失敗でも成功の確定でもありません。エンドツーエンドで厳密に 1 回だけ効果を発生させるには、プロバイダー固有の冪等性を利用する、またはプロバイダーの正式な記録と照合する必要があります。
スタック
SafeAgent は、エージェント実行整合性スタックにおける、耐久性のあるクレームおよび重複抑止レイヤーです。
Polaris (commit-gated authorization)
└── AgentGraph safety verdict (pre-execution safety gate)
└── SafeAgent (durable claim guard) ← you are here
└── Mycelium Trails (on-chain anchor)
各レイヤーは独立して作成され、独立して検証できます。どのレイヤーもほかのレイヤーを信頼しません。
API
POST /claim
アクションをゲートします。初回の呼び出しでは PROCEED を返し、再度呼び出した場合は SKIP を返します。
リクエストボディ:
{
"request_id": "order:TQQQ:buy:6:2026-05-19T13:31:00-04:00",
"action": "order"
}
request_id— エージェントが実行する内容から導出される安定した識別子です。同じ入力なら同じキーになります。action— ゲートするアクションの種類(例:payment.send、email、trade、order)
レスポンス:
{ "status": "PROCEED", "request_id": "order:TQQQ:buy:6:2026-05-19T13:31:00-04:00" }
同じペイロードで再試行します:
{ "status": "SKIP", "request_id": "order:TQQQ:buy:6:2026-05-19T13:31:00-04:00", "existing": {...} }
POST /settle/{request_id}
アクションの実行後にクレームを確定します。ステータスを PENDING から COMMITTED に進めます。
curl -s -X POST https://safeagent-production.up.railway.app/settle/order:TQQQ:buy:6:2026-05-19T13:31:00-04:00 \
-H "Content-Type: application/json" \
-d '{"result":{"status":"completed","txid":"abc123"}}'
{ "status": "committed", "request_id": "..." }
POST /claim/test
無料のテスト用エンドポイントです。支払いは不要で、同じロジックを使用します。IP ごとの合計呼び出し回数は 10 回までです。
curl -s -X POST https://safeagent-production.up.railway.app/claim/test \
-H "Content-Type: application/json" \
-d '{"agent_id":"bot-1","action_type":"order","scope":"TQQQ:buy:6:bar:2026-05-19T13:31:00-04:00"}'
{ "status": "PROCEED", "request_id": "...", "test": true, "calls_remaining": 9 }
GET /audit
クレームの全履歴です。agent_id、action、status、またはタイムスタンプ範囲で絞り込めます。
curl "https://safeagent-production.up.railway.app/audit?agent_id=bot-1&status=COMMITTED"
{"items": [...], "total": 5, "limit": 100, "offset": 0}
パラメーター:agent_id、action、status、from_ts、to_ts、limit(最大 1000)、offset。
統合
Python
import requests
def claim(request_id: str, action: str) -> dict:
r = requests.post(
"https://safeagent-production.up.railway.app/claim",
headers={"Content-Type": "application/json"},
json={"request_id": request_id, "action": action}
)
return r.json() # {"status": "PROCEED"|"SKIP", "request_id": "..."}
def settle(request_id: str, result: dict) -> dict:
r = requests.post(
f"https://safeagent-production.up.railway.app/settle/{request_id}",
headers={"Content-Type": "application/json"},
json={"result": result}
)
return r.json()
# Usage
response = claim("payment:customer-123:invoice-456", "payment.send")
if response["status"] == "PROCEED":
result = send_payment(...)
settle("payment:customer-123:invoice-456", {"status": "completed"})
elif response["status"] == "SKIP":
print("Already executed, skipping")
SaaS(Stripe / メール / Webhook)
本番環境の Stripe PaymentIntents には、SafeAgent Boundary に記載されている、許可証でゲートするアダプターを使用してください。 このアダプターは Stripe の認証情報をエージェントの外部に保持し、消費済みの許可証 ID をネイティブの冪等性キーとして Stripe に渡し、プロバイダーとの相関関係を永続化し、Webhook の署名を検証し、正式な情報の読み戻しをサポートします。以下のシンプルな claim/settle のスニペットは汎用 API のみを示すものであり、プロバイダーアダプターではありません。
examples/stripe_payment_intent_guard.py の制限付きテストモードの例を実行してください。
検証記録 には、エンドツーエンドで確認された結果として、Stripe サンドボックスの PaymentIntent が 1 件成功したこと、同一リクエストの再送がプロバイダーへの送信前にブロックされたこと、retrieve によって正式に照合されたことが記録されています。
import requests
def safe_stripe_charge(customer_id, amount, idempotency_key):
request_id = f"stripe:charge:{customer_id}:{idempotency_key}"
response = requests.post(
"https://safeagent-production.up.railway.app/claim",
headers={"Content-Type": "application/json"},
json={"request_id": request_id, "action": "stripe_charge"}
).json()
if response["status"] == "SKIP":
return response.get("existing")
charge = stripe.PaymentIntent.create(
amount=amount,
currency="usd",
customer=customer_id,
idempotency_key=request_id,
)
requests.post(
f"https://safeagent-production.up.railway.app/settle/{request_id}",
headers={"Content-Type": "application/json"},
json={"result": {"charge_id": charge.id}}
)
return charge
Webhook の重複排除 — 安定したイベント ID によって、確定済みの結果に対する重複処理を抑止します。未解決の PENDING イベントは、調査のためブロックされたままになります。
def handle_stripe_webhook(event):
response = requests.post(
"https://safeagent-production.up.railway.app/claim",
headers={"Content-Type": "application/json"},
json={"request_id": event["id"], "action": "stripe_event"}
).json()
if response["status"] == "SKIP":
return {"ok": True}
provision_subscription(event)
requests.post(
f"https://safeagent-production.up.railway.app/settle/{event['id']}",
headers={"Content-Type": "application/json"},
json={"result": {"processed": True}}
)
CrewAI
from crewai import Agent
import sqlite3
guard_con = sqlite3.connect("safeagent_orders.db", check_same_thread=False)
# ... same guard pattern, keyed on tool_name + input hash + session_id
PR crewAIInc/crewAI#5822で、プラグイン可能な冪等性バックエンドが追加されました。SafeAgentのSQLiteスキーマと互換性があります。
WisePick
WisePickは、エージェントランタイム向けの決定論的なルーティングレイヤーです。この統合では、機能の選択と永続的で冪等な実行を分離しています。WisePickは「何を」「どのプロバイダーで」実行するかを判断し、SafeAgentはこの論理的な処理がすでに実行済みかどうかを判断します。
スタック:
WisePick /v1/decide → DashClaw → SafeAgent → Mycelium Trails → Base/Arbitrum
- アダプター:
adapters/safeagent_adapter.py - 統合ドキュメント:
docs/integrations/safeagent.md - 統合スレッド:SafeAgent #9
ローカルガード(組み込みSQLite)
ネットワーク呼び出しなしで、同じ永続的なクレーム動作を実現できます。これを任意のPythonエージェントに追加してください。
import json
import sqlite3
_SA_DB = "safeagent_orders.db"
_sa_con = sqlite3.connect(_SA_DB, check_same_thread=False)
_sa_con.execute("""CREATE TABLE IF NOT EXISTS orders (
request_id TEXT PRIMARY KEY,
result TEXT,
status TEXT DEFAULT 'PENDING',
created_at TEXT DEFAULT (datetime('now'))
)""")
_sa_con.commit()
def place_order_with_guard(symbol, qty, side, bar_ts):
request_id = f"order:{symbol}:{side}:{qty}:{bar_ts}"
cur = _sa_con.execute(
"INSERT OR IGNORE INTO orders (request_id, status) VALUES (?, 'PENDING')",
(request_id,)
)
_sa_con.commit()
if cur.rowcount == 0:
row = _sa_con.execute(
"SELECT status, result FROM orders WHERE request_id = ?",
(request_id,)
).fetchone()
if row and row[0] == 'COMMITTED':
print(f"SAFEAGENT SKIP: {request_id}")
return row[1]
raise RuntimeError(
f"SAFEAGENT BLOCKED: unresolved PENDING claim for {request_id}"
)
result = place_order(symbol, qty, side)
_sa_con.execute(
"UPDATE orders SET status='COMMITTED', result=? WHERE request_id=?",
(json.dumps(str(result)), request_id)
)
_sa_con.commit()
return result
ケーススタディ:クラッシュ後の再試行による重複の防止
あるトレーディングボットが、TQQQを6株買う成行注文を送信します。ブローカーは注文を受け付けます。ボットは状態を更新する前にクラッシュします。再起動後、同じシグナル、同じバーで、ボットは再び注文を送信します。ブローカーは注文を二重に約定させます。エージェントは6株保有するつもりだったのに、12株を保有することになります。
これは理論上の話ではありません。注文の送信から状態の永続化までの間に未処理の例外が発生すると、どのような場合でも起こり得ます。
SafeAgentは、ブローカーにアクセスする前に安定したキーを導出します。
request_id = "order:TQQQ:buy:6:2026-05-19T13:31:00-04:00"
初回呼び出し:PROCEED。副作用が実行されます。settleによってCOMMITTEDが書き込まれます。
クラッシュ後の再試行:SKIP。ブローカーに再びアクセスすることはありません。
ケーススタディ:ライブ環境での重複ブロック — 2026年5月21日
フルスタック(DashClaw、SafeAgent、Mycelium Trails、Base/Arbitrum、ブローカーのAlpaca)を使用したライブセッションで、SKIPイベントを6件確認しました。
- 0942 ET:重複するTQQQの買い注文、数量=6をブロック、$452
- 0947 ET:重複するTQQQの追加注文、数量=6をブロック、$452
- 0949 ET:反転時の重複するTQQQの売り注文、数量=12をブロック、$902
- 1000 ET:重複するTQQQのエントリー、数量=6をブロック、$454
- 1014 ET:V20反転時の重複するTQQQの売り注文、数量=18をブロック、$1,350
- 1106 ET:スケールイン中の重複するSQQQの追加注文、$43
セッション全体のデータ:https://gist.github.com/azender1/b9112b6519c935df4a75cb05cd250e26
適合性
SafeAgentは、A2Aのクロス実装適合性コーパスに参加しています。4つの独立した実装で、フィクスチャをバイト単位で検証しています。
- kenneives (agentgraph) — verifier-attestation-v0:30/30 ✓
- evidai (LemonCake) — gated-preflight-v1:33/33 ✓
- haroldmalikfrimpong-ops (agentid) — 独立した検証者 ✓
- SafeAgent — claim-lifecycle conformance v1.1 ✓
A2A #1920はクローズされました。4つすべての実装が鮮度とリプレイの要件を満たしています。
正規のaction_ref導出
JCS(RFC 8785)。argentum-core、Nobulex、APSとの整合性があります。
import hashlib
import rfc8785
def compute_action_ref(
agent_id: str,
action_type: str,
scope: str,
timestamp: str, # RFC 3339 UTC, 3-digit ms: "2026-05-15T10:00:00.123Z"
) -> str:
payload = {
"agent_id": agent_id,
"action_type": action_type,
"scope": scope,
"timestamp": timestamp,
}
canonical = rfc8785.dumps(payload)
return hashlib.sha256(canonical).hexdigest()
timestampは、ミリ秒の桁数が正確に3桁のRFC 3339形式のUTC文字列です。末尾のZは必須です。
適合性フィクスチャ:giskard09/argentum-core tag action-ref-v1.0
デプロイ
Railway · サーバーレスOFF · 常時稼働
PyPI:pip install safeagent-exec-guard
npm:npm install n8n-nodes-safeagent
MCP Registry:io.github.azender1/safeagent
ダッシュボード:safeagent-dashboard-2.vercel.app
ライセンス
Apache-2.0
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。