「行動するAI」を安全に導入する

こんにちは。エンジニアの小川です。

今回は、Agentforce で AWS の IAM ユーザーを払い出すエージェントを社内向けに作った話を共有します。「行動するAI」 を安全に導入するにはどうすればいいか、という話です。

これまでフレクトのクラウドblogでは、RAG やハイブリッド検索といった「ユーザーの質問に答える」エージェントの記事を投稿してきました。今回は少し切り口を変えて、エージェントが会話の結果として リソースを作る ケースを扱います。

まず前提として、「行動するAI」 の会話の部分は、LLM が担う確率的な振る舞いです。「だいたいやってくれる」けれど「必ずこうする」という保証はありません。一方で、リソースを払い出すアクションの部分は、きっちり固めておきたい。「許可した人だけ」「許可した範囲だけ」「必ず記録する」を確実に守らせたいわけです。

そこで本記事では、会話は確率的、アクションは決定論的に、2つを分けて設計するという考え方を軸に、次の3つの観点に分けて紹介します。

  1. 権限を絞る(最小権限)── 誰が起動でき、自動化基盤自身がどこまで影響力を持つか
  2. 起きたことを必ず捕捉・追跡する(可観測性)── 何が起きたか必ず管理者が気づけて、後から追える
  3. 上の2つを確実に効かせる ── 確率的な LLM を、決定論的なしくみで固める

題材:検証環境向けに IAM ユーザーの払い出しを自動化

社内には検証用のAWS環境があり、そこを使うために IAM ユーザーの発行を申請する、という運用があります。従来は管理者が手作業でユーザーを作り、グループに割り当て、初期パスワードを本人に伝える、という流れでした。

これを Agentforce で自動化しました。ゴールを一言で言うと、こうなります。

申請者が Slack で「検証環境の IAM ユーザーを作ってほしい」と頼むと、エージェントが必要事項を聞き取り、申請者の確認を経て、IAM ユーザーを払い出して初期パスワードを本人に DM で届ける。

図A:全体構成図

ポイントを補足します。

  • 入口は Slack。 申請者は普段使っている Slack から、接続されたエージェントに話しかけます。新しい画面を覚える必要はありません。
  • 頭脳は Agentforce の Employee Agent。 会話による意図解釈と情報収集を担います。
  • 行動は Salesforce Flow。 Agent Script から見た実行単位はフローで、フローが外部 API を呼び、その先の Lambda が実際の IAM 操作を行います。

題材のAIエージェントの利用イメージをつかむために、下に画像を貼っておきます。

エージェント利用の流れ


1. 権限を絞る ── 誰が・何が、何を許されるか(最小権限)

「行動するAI」 を安全に入れるための出発点は、AI の推論をどう制御するかよりも手前にあります。

そもそも誰がこの AI を動かせるのか。そして、動いた AI(とその裏側の自動化のしくみ自身)はどこまでのことを起こせるのか。この2つを絞るところから始めます。

ここでは前者を「人間側」、後者を「機械側」と呼んで分けて見ていきます。

人間側 ── 動かせる人を絞る

入口を Slack のチャンネルに限定し、DM を無効化する

会話の入口は、社内の特定ワークスペースの、さらに特定チャンネルだけに絞っています。ワークスペースには社員しかいないので、外部からのアクセスはここで止まります。

もうひとつ、エージェントへの DM も無効化しています。Slack App の設定画面(api.slack.com/apps)で、App Home の Messages Tab(Agent / Assistant が ON の場合は Chat Tab)をオフにするだけです。これで、ユーザーがエージェントに直接メッセージを送ろうとしても送信されません。

狙いは、申請を指定チャンネルに集約することです。管理者の見えないところで、こっそり操作を依頼されるのを防ぎます。

Slack App設定画面でエージェントのDM受付をオフにする

Slackでエージェントに直接アクセスした画面。DMできてしまいそうに見えるが、メッセージを入力して送信ボタンを押しても、入力した内容がクリアされるだけで何も起こらない。

申請者を権限セットで絞る

ユーザーからのアクセスを許可したいエージェントを権限セットの「エージェントアクセス」に登録し、この権限セットをユーザーに割り当てると、そのユーザーは指定のエージェントにアクセスできるようになります。

そしてこの権限設定は、Slack からの会話可否も制御できます。権限セットが割り当てられていないユーザーがエージェントにメンションしても、接続できていないときのメッセージが返るだけで、会話は始まりません。

※なお、この挙動については実は複雑で、環境やシステムの組み方によっても変わります。今回は社内での検証結果を踏まえたうえで、慎重にテストを行ってこのような挙動になることを確認しています。

利用者についても少しだけ触れておきます。今回は代理申請を想定していて、申請できるのは社員だけですが、払い出す相手(利用者)は社員とは限りません(ビジネスパートナーの分を社員が申請することがあります)。そのため利用者が正しいかどうかは、フローの中で利用者のメールアドレスを User レコードと照合し、ヒットしなければ API コールに進ませません。会話でどう言いくるめられても、登録外のユーザーには払い出せないよう物理的に止めます。

会話できても、権限がなければ「作る」には届かない

ここで大事なのは、会話できることと、リソースを作れることは別だという点です。

従業員エージェントは、話しかけたユーザー本人の権限で動きます。エージェントが Salesforce 上でできることは、そのユーザーの権限の範囲に限られます。

つまり、アクションに設定したフローを実行する権限と、そのフローの中で外部 API を呼ぶ権限。この2つがなければ、会話はできても「作る」ところまでは到達しません。

図B:アクセス制御の層構造

機械側 ── 自動化基盤自身が起こせることを絞る(=ブラスト半径を狭める)

リソースを作るエージェントは、裏側の自動化基盤に権限を与えて行動を許可しています。ここが緩いと、何かが暴走したときの影響範囲(ブラスト半径)が一気に広がります。

Lambda 実行ロールを最小権限にする

そこで Lambda の実行ロールは、「やってよいこと」だけに絞りました。

  • 作れるユーザーを、命名規則のプレフィックスが付いたものだけに限定(規約外の名前では作れない)
  • 入れられるグループを、管理対象のものだけに限定
  • 読めるのは、Slack トークンが入った SSM の1パラメータだけ
  • KMS は kms:ViaService 条件を付け、SSM の復号以外には使えないようにする

ポイントは、払い出す側の権限そのものを絞っておくことです。仮に想定外の動きをしても、現実に起こせる事故の範囲があらかじめ狭められている状態を作ります。

ちなみに「管理者相当の権限が必要か?」は会話の中で聞き取りますが、その答えをそのまま権限割り当てには使いません。LLM にはフラグの判定までを任せ、「このフラグならこのグループ」という対応づけは Lambda 側に固定で持たせています。判断の入口は会話でも、実際の割り当ては決め打ち、というわけです。

機密情報を持たせない

最小権限は、操作の権限だけの話ではありません。情報についても「要らないものは持たせない」を徹底して、パスワードはエージェントに渡さないようにしています。

当初は、Lambda が作ったパスワードを API のレスポンスで Salesforce に返し、エージェントがそれを DM に投稿する、という案もありました。ただこの方式だと、パスワードが API Gateway → Salesforce → Agentforce と渡り歩き、ログに残りうるポイントを何か所も通過してしまいます。エージェントがパスワードを知ってしまうこと自体も、扱いを完全には制御しきれず不安が残ります。

そこで、パスワードの通り道をエージェントから切り離しました。今は、Lambda が IAM ユーザーを作ったあと、自分で Slack のトークンを取り出し、Slack API を直接叩いて本人へ DM します。エージェントはパスワードに一切触れません。申請者に返すのは「DM でログイン情報を確認してください」というメッセージだけです。

結果として、パスワードが通る経路は Lambda の中だけになりました。あちこちのログ対策を個別に積み上げる必要もなくなっています。


2. 起きたことを必ず捕捉・追跡する ── 可観測性

権限で「起こせること」を絞ったら、次は、起きたことを取りこぼさないための層です。狙いはシンプルで、何かあったとき管理者が必ず気づけて、後から必ず追えるようにすることです。

通知の強制

エージェントがリソースを作ったら、その事実を管理者が必ず受け取れるようにしています。

ひとつ注意点があります。エージェントが申請スレッドに返す完了通知は、エフェメラルメッセージ(「あなたにだけ表示されています」と出るあれ)です。申請者が「Share in Thread」を押さない限り、管理者には見えません。

そこで、管理者への通知はエージェント任せにせず、Lambda が利用者へのパスワード DM と同じタイミングで直接送るようにしました。見えないところで依頼が成立しても、通知だけは必ず飛びます。

記録の強制

すべての申請は、Salesforce のカスタムオブジェクト(申請台帳)に記録されます。

記録項目 内容
申請者 誰が申請したか(Salesforce ユーザー)
利用者 誰のために払い出したか
アカウント種別 個人 / PJ共用
権限レベル 開発者 / 管理者
実行結果 成功 / 失敗 / 重複(既存ユーザー検知)
備考 特記事項、メモ

この記録は、API コールが成功しても失敗しても必ず作られます。正常系でも異常系でも、とにかくログが残る。失敗したときも、誰が何を頼んで、どこで転んだのかを管理者が追えます。

承認ゲートは設けていない

「実行を物理的に止める管理者承認」は、今回は入れていません。管理者に通知が必ず届き、実行記録が必ず残り、その記録には必ず申請者(社員)が付く——この3つが揃っていれば、検証環境の払い出しについては通知ベースで十分だと判断したためです。今後別のユースケースを実装する際に、承認プロセスが必要であれば、そこで追加する方針としています。


3. 上の1・2を確実に効かせる ── 確率的LLMを決定論的に固める

ここまでの「権限を絞る」「必ず捕捉・追跡する」を、確率的にしか動かない LLM の上でどう確実に効かせるか。 AgentScriptの構文を含めた次の3つを駆使して決定論的に固めています。

  • available when(アクションが使える条件を絞る)── 「この条件のときだけこのアクションを使ってよい」というガードです。ただし条件を満たしていても、1フローの実行権限がなければ動きません。ロジックのゲートと権限のゲート、二重で初めて発火します。

  • if(条件が合えば必ず実行する)── 「LLM が気を利かせてやってくれたら」に頼らず、条件が合ったら確実に実行させます。たとえば、出力が特定の状態なら必ずエスカレーション通知を出す、変数をリセットする、といった処理です。

  • ロジックを LLM の外に出す ── 決定論的な分岐や「必ずやる処理」は、フローや Apex の中に閉じ込めます。API を直接アクションにすることもできますが、フロー / Apex に包むことで細かい条件分岐でガードをかけられます。 そして、1で触れた利用者チェックと、2で述べた記録の強制を、最終的に担保しているのがこの層です。

利用者チェック(1):フローの中で利用者のメールアドレスを User レコードと照合し、ヒットしなければ API コールに進ませません。会話でどう言いくるめられても、登録外のユーザーには払い出せないよう物理的に止めます。

記録の強制(2):フローの中で、呼び出したAPIの結果が成功でも失敗でも記録だけは必ず残します。異常系を通っても追跡できる状態を保ちます。


まとめ

今回は、Agentforce で IAM ユーザーを払い出すエージェントを題材に、「行動するAI」 をどう安全に設計するかを紹介しました。

出発点は、確率的な会話と決定論的なアクションを分けて考えることでした。その上で押さえたのは、次の3つです。

  • 権限を絞る: 動かせる人を絞り(Slack チャンネル・DM・権限セット・フロー / API 権限)、しくみ自身が起こせることも絞る(Lambda ロールの最小権限・パスワードを持たせない)
  • 必ず捕捉・追跡する: 通知と記録を、成功でも失敗でも強制する。承認ゲートは今回は見送り
  • 確率的な LLM を決定論で固める: available when / if / ロジックをフロー・Apex へ。ここで利用者チェックと記録を担保する

今回はこれで以上となります。最後までお読みいただき、ありがとうございました。