aidlc-workflowsのbugfixスコープでバグ修正してみた — AIとのやり取りと判断の記録
はじめに
こんにちは、カンリー店舗集客チームの三上です。
以前、AWSが公開しているaidlc-workflowsを使った機能開発の設計フェーズで、どのようにAIの成果物をレビューし、チームで意思決定しているかを紹介しました。
zenn.dev
今回は少し趣向を変えて、aidlc-workflowsのbugfixスコープを使って、実際にバグ修正をリリースするまでの過程を紹介したいと思います。
対象となったのは、Googleビジネスプロフィールに連携するデータのCSV一括更新で、メニューやサービスの説明文の文字数制限が、入力経路によって食い違っていた不具合です。
当初のチケットには修正方針も書かれており、そのとおりに実装すればよさそうに見えました。しかし、ワークフローに沿ってコード調査や要件分析を進めるうちに、その修正方針自体を見直すことになりました。
AIとのやり取りの詳細を振り返ってみると、AIの調査結果をそのまま採用することもあれば、根拠を問い直し、提示された選択肢を選ばずに調査をやり直すこともありました。
この記事では、そうしたAIとのやり取りや判断を、実際の作業の流れに沿って紹介します。
ぜひ「自分だったらここでどう判断するだろう?」と考えながら読んでみてください。
bugfixスコープのステージ構成
本題に入る前に、aidlc-workflowsの進め方について触れておきます。以下のステージ構成は、aidlc-workflows v2.5.10での内容です。
aidlc-workflowsでは、開発の工程が「ステージ」という単位に分かれています。ステージは全部で32個あり、5つのフェーズ(Initialization / Ideation / Inception / Construction / Operation)に属しています。
どのステージを実行するかは「スコープ」で決まります。bugfixスコープでは、32ステージのうち7ステージだけを実行し、残りはスキップします。
bugfixスコープは、既存コードの理解、修正要件の明確化、実装と検証に絞った構成です。新機能開発で実施する市場調査やアーキテクチャ設計などはスキップします。
フェーズ
ステージ
役割
Initialization
workspace-scaffold / workspace-detection / state-init
作業記録の置き場を用意し、対象のコードベースの言語やフレームワークを判定して、どのステージを実行するかの計画を作ります。自動で進みます
Inception
reverse-engineering
既存のコードを読み、構成・API・依存関係などをドキュメントにまとめます。以降のステージは、ここでまとめた内容を前提に進みます
Inception
requirements-analysis
依頼内容を分析し、曖昧な点や判断が必要な点を洗い出して、要件定義書にまとめます
Construction
code-generation
まず実装計画を作って人の承認を取り、そのあとで計画に沿ってコードを書きます。最後に変更内容のまとめを残します
Construction
build-and-test
ビルドとテストの手順書を作り、テストを実行して結果をまとめます
1つのステージの回し方
大まかな流れは以下の通りです。
判断が必要なステージ(今回はrequirements-analysisとcode-generation)は、AIが成果物を一気に作って人が最後に承認する、という流れではありません。AIが発行した設問を通して、人と認識をすり合わせながら成果物を作っていきます。
設問と回答はファイルとして残るので、あとから「なぜその判断になったのか」を辿ることもできます。
修正するバグについて
Googleビジネスプロフィール(以下、GBP)では、店舗ごとにメニューの情報や提供するサービスの情報を設定することができます。
不具合の内容は、「GBPに連携するメニューとサービスの説明文について、CSV一括更新時の文字数制限が、ほかの入力経路と食い違っている」というものでした。
チケットには、メニューとサービスそれぞれについて、次の修正方針が記載されていました。
メニュー:CSV一括更新時の文字数制限を300文字から1000文字に変更する
サービス:メニューと対称的な構造になっているため、同様に1000文字に変更する
つまり、メニューとサービスの両方で、CSV一括更新時の上限を1000文字に引き上げるという方針です。修正内容がチケットに書かれていたので、最初はその方針に沿って実装を進めればよさそうに見えました。
ただ、今回はaidlc-workflowsのbugfixスコープを使うため、すぐにコードを変更するのではなく、まずreverse-engineeringで既存コードの調査から始めることになりました。
reverse-engineering - コード調査で修正方針に疑問が生じた
最初のreverse-engineeringステージでは、AIが既存のコードを調査し、システムの構成や関連する処理を整理していきます。
調査を始めた段階で、AIからチケットに書かれた修正方針について指摘がありました。
🤖 AI
メニュー側(1000)とサービス側(300)を同じ値に揃えてよいのかという論点で、チケットの期待値そのものを変える可能性があります。
チケットでは、メニューとサービスが対称的な構造になっていることを理由に、両方の上限を1000文字へ変更する方針になっていました。
しかし、コードを確認すると、画面の入力欄ではメニューが1000文字、サービスが300文字と、もともと文字数上限が違っていました。さらに、GBPではメニューとサービスは別のAPIで扱われており、媒体側の仕様から見ても、上限が同じとは限りません。
同じ下調べでは、チケットに書かれていない修正候補も見つかっていました。サービスの説明文を受け付けるAPIのバリデーションで、上限が1023文字になっている箇所です。
この時点では、どちらの仕様が正しいのかまでは分かっていません。画面側の実装が正しいとも限りませんし、チケットの修正方針が間違っているとも断定できません。
修正内容が決まっているように見えたチケットでしたが、実装に入る前に、その前提を確認する必要が出てきました。
requirements-analysis - 前提を疑い、調査をやり直した
requirements-analysisでは、reverse-engineeringで出てきた疑問を、一次資料にあたって確かめていきます。AIはGBPの公式ドキュメントを引き、確認した事実と、人に判断してほしい設問を整理していきました。
最初にまとまった推奨案は、サービスの文字数上限を1000文字にせず、300文字のまま据え置くというものでした。
ただ、ここからの数時間で、AIが示した根拠を私が問い直す場面が何度もありました。
AIが参照したAPIは正しいのか
最初に気になったのは、AIがサービスの仕様の出典として挙げていたAPIです。
店舗集客がサービスの更新に使っているのは、Business Information APIのlocations.patchですが、AIは「ServiceList API」を参照していました。
このプロダクトに関わって3年ほど経ちますが、そんな名前のAPIを扱っている実装は見たことがありませんでした。
🤔 三上
ServiceList APIって、どのAPIを指してます?
店舗集客で使っているAPIの情報を添えて聞き返すと、非推奨となった旧APIを出典にしていたことが分かりました。現行APIを確認し直しても値は変わりませんでしたが、参照先は修正してもらいました。
この調査の中で、APIドキュメントの記載が英語版と日本語版で異なっていることもわかりました。英語版にはサービスの説明について「250文字以下を推奨」とありますが、日本語版には「半角250文字(全角125文字)以下」と書かれています。
この違いを確認したうえで、AIから本番データとエラーログを調査する提案がありました。
「GBPは250文字を強制していない」は本当か
AIは過去30日分のエラーログを調査し、文字数超過による更新エラーが0件だったと報告しました。また、DBを確認し、全角125文字を超えるデータが実際に既に存在していることもわかりました。
調査結果を踏まえて、AIは次のように結論づけました。
🤖 AI
GBP の 250 は推奨であって強制されておらず、全角 125 文字超(半角に換算すると250文字を超えるデータ)がありながら 30 日間の拒否は 0 件でした。
AIはこの結論を根拠に、サービスの上限を300文字のまま据え置く方針で確定させようとしていました。
しかし、それが根拠として十分なのかが気になりました。その理由は、ログ調査では対象期間を30日に絞っていたのに対して、DBで実行したクエリは作成時期を問わず、今ある全データのうち対象が何件あるかを数え上げるものだったからです。
🤔 三上
過去30日で弾かれたのは0件だったが、たまたま直近30日で発生していなかっただけの可能性はないですか?DBの取得結果は全期間を対象としています。ログ調査の期間内に作成されたデータがあるなら納得できますが、DBの結果と照らし合わせた結論でしょうか?
30日間エラーがなかったことと、GBPが文字数上限を強制していないことは別の話です。AIも推論が飛躍していたことを認め、結論を撤回しました。
🤖 AI
DBの取得結果には作成日を含んでおらず、確認不足でした。「強制されていない」は取り下げ、未確認に戻すべきです。
AIは誤った記述の修正計画とあわせて、未確認の点を埋めるための追加調査の案を3つ示しました。
A. 未確認のまま追加調査をしない
B. ログの検索期間を90日に広げてもう一度確かめる
C. GBPから取り込んだデータとDBの値を付き合わせる
提示された3択を選ばず、前提から調べ直す
AIが示した追加調査案を受け入れず、このように返しました。
🤔 三上
調査自体やり直したいです。前提となる事実全体を調べ直したいです。
この時点までに、旧APIを出典にしていたこと、観測したデータから結論を急いでいたことなど、根拠や推論の誤りが続けて見つかっていたからです。
前提や根拠を誤ったまま判断してしまうと、「GBPのメニュー・サービスの情報を適切に更新できるようにする」というバグ修正の本来の目的を果たせなくなる可能性があります。修正方針を立てる上で、納得できる根拠を持っておくべきと考えました。
GBPの公式ドキュメント、実際のAPI呼び出し経路、DB、エラーログ、チケットに記載された値などを改めて確認しました。エラーログも、過去30日ではなく1年分を調べました。
再調査によって、次のことが確認できました。
公式ドキュメントの文字数には、上限ではなく推奨値として記載されているものがあった
過去1年分のログでも、文字数超過による更新エラーは確認できなかった
店舗集客からGBPへ送信する経路では、GBPへの更新が成功しなければDBに保存されないことが確認できた
GBPから取り込んだデータに、全角で250文字を超えるサービスの説明文が存在した
推奨値を超える文字数の説明文が実際に存在することは確認できましたが、APIが受け付ける上限はまだ分かっていませんでした。そこで、GBPの管理画面に手がかりを求めることにしました。
GBPの管理画面でも上限を確認する
GBPの管理画面を確認すると、サービスの説明文は300文字、メニューの説明文は1000文字までしか入力できない作りになっていました(いずれも全角半角を問わない)。店舗集客の画面側で設定されていた上限と一致しています。
さらに、GBP APIを使った更新も試し、画面と同様にサービスの説明文は300文字、メニューの説明文は1000文字まで受け付けられることを確かめました。
ここまでの結果を踏まえ、サービスの説明文は300文字のまま変更しない方針にしました。最初にAIが示した方針と同じ結論になりましたが、再調査を入れたことで、その判断を支える十分な根拠を得ることができました。
一方、サービス名は管理画面で120文字までしか入力できませんでしたが、公式ドキュメントの推奨値は140文字でした。
AIは当初、サービス名を「今回の修正の対象外」としていました。しかし、ユーザーが入力できる文字数に関わるため、PdMに方針を確認したいと考えました。
翌日、PdMとも修正方針を合意した上で、当初修正対象にしていなかったサービス名も、GBPの管理画面上の仕様に合わせて120文字の制限をつけることに決めました。
レビュー用AIの指摘を、実際のデータで判断する
ここまでの調査結果を踏まえて、無事AIが発行した設問にすべて回答できました。その回答をもとに要件定義書が作成されると、レビュー用のAIが内容を確認します。
AIのレビューの中で、OpenAPIに文字数上限を追加することについて、次の懸念が指摘されました。
OpenAPIに文字数上限を追加すると、画面側の入力チェックにも自動で反映される
すでに文字数上限を超えたデータを持つ店舗では、編集画面で理由が表示されないまま保存ボタンが押せなくなる可能性がある
つまり、OpenAPIを変更することで、「文字数の上限を超えている」という事実がわからないまま、ただ保存ボタンが押せない状態となって困るユーザーがいるのではないかという懸念です。
まずはこの懸念に該当するデータが実際に存在するのか確認した上で判断したいと思い、DBを確認しました。幸い、懸念のケースに該当するデータはありませんでした。
既存データへの影響が出ないことが確認できたので、AIが指摘した懸念を踏まえたうえで、OpenAPIにも上限を追加する方針にしました。
このステージで決まったこと
要件は、チケットに書かれていた修正方針から次のように変わりました。
メニュー:CSV一括更新の上限を300文字から1000文字に引き上げる(チケットどおり)
サービス:CSV一括更新の上限は300文字のまま据え置く(チケットでは1000文字に変更する予定だった)
チケットに書かれていなかったもの:APIのサービス説明文の上限を1023文字から300文字に変更し、サービス名の上限を120文字に揃える。OpenAPIに文字数上限を追加し、仕様書の記述も修正する
このように、最終的にはチケットに書かれた修正方針を一部見直し、当初の対象に含まれていなかった箇所も修正することになりました。
code-generation / build-and-test - 計画を立ててから実装し、テストする
店舗集客では、GBPと連携するデータの更新が、次のように複数のシステムにまたがっています。
今回の修正も、それぞれに手を入れる必要がありました。requirements-analysisで決めた修正方針と修正対象の対応は以下の通りです。
システム
役割
修正内容
OpenAPI定義
APIの仕様の正本。変更すると、管理画面とAPIのコードに自動で反映される
メニュー・サービスの説明文とサービス名に、文字数の上限を追加
管理画面
店舗の情報を編集する画面
OpenAPI定義の変更が自動で反映されるのを確認
店舗集客のAPI
管理画面からの更新を受け付け、GBPへ送る
サービスの説明文の上限を1023文字から300文字に、サービス名の上限を120文字に変更
worker
CSV一括更新を処理する
メニューの説明文の上限を300文字から1000文字に変更
仕様書
機能の仕様をまとめたドキュメント
上限値の記述を修正
code-generationでは、AIがいきなりコードを書くのではなく、まず実装計画を作ります。今回の計画では、複数のシステムにまたがる修正を5本のPRに分け、どの順番でマージするかまで整理していました。
計画を承認すると、AIが計画に沿ってコードを書き、最後にレビュー用のAIが変更内容を確認します。
分からない懸念は、分かるまで聞く
GBPのサービスには、Googleがあらかじめ用意している定型のサービス(StructuredServiceItem)と、店舗が自由に登録するサービス(FreeFormServiceItem)の2種類があります。
店舗集客の画面から編集できるのは、FreeFormServiceItemだけです。要件分析で本番データの文字数を確かめたのも、こちらのサービスでした。
実装の途中で、コードを書いていたAIから懸念が1つ上がってきました。
店舗集客のAPIでサービス名の上限を120文字にすると、文字数を確かめていないStructuredServiceItemの名前にも上限がかかってしまう、という内容です。
しかし、StructuredServiceItemは店舗集客では扱っておらず、GBPへのデータ連携の対象外とした実装になっています。対象外のデータについて何を心配しているのか、私にはすぐには分かりませんでした。
🤔 三上
ちょっと言ってる意味がわからなかったです。懸念事項について、もう少し詳しく説明してもらえますか?
聞き返すと、AIはあらためてコードを確認し、懸念を取り下げました。
🤖 AI
サブエージェントの読み違いで、実害の懸念は成立しません。撤回します。
懸念の出どころは、実装を担当したAIが、仕様書の1文から推測したものでした。AIの調べ直しで、バリデーションはFreeFormServiceItemのみに適用されており、StructuredServiceItemへの影響がないことがわかりました。
分からないまま「OK、それで」と進めていたら、不要な対応を追加していたかもしれません。
ドキュメントの書き方は、マージの順番で解決する
実装のあと、レビュー用のAIから次の指摘がありました。
仕様書に、まだリリースしていない新しい上限値が、現在の値として書かれている
AIの修正案は、仕様書に「リリース前の値はこうだった」という注記を書き足すものでした。
ただ、そうするとリリースしたあとに、注記を消すためにもう一度仕様書を直すことになります。
🤔 三上
リリース前である旨を記載するとリリース後に再度修正が発生するので手間です。マージ順序でカバーできないですか
最終的に、仕様書のPRは、実装のPRがすべてリリースされたあとにマージする順番にしました。
仕様書を書き直すのではなく、マージの順番で整合を取った形です。
最後にbuild-and-testで、ビルドとテストの手順書を作り、テストを実行しました。
バグ修正の意図通りに実装されているか確かめるため、上限ちょうどの文字数と1文字超えた文字数を確かめる単体テストを追加し、ローカル環境とCIで確認しました。
ワークフローはここまでで、検証環境での動作確認とリリースは、ワークフローの外で進めました。会議や他のタスクを複数並行している中でも、着手から1スプリント(1週間)でリリースすることができました。
おわりに
今回は、aidlc-workflowsのbugfixスコープを使って、バグ修正をリリースするまでの過程を紹介しました。
チケットに書かれた修正方針を見直し、当初は対象に含まれていなかった箇所も修正してリリースできました。
修正方針の見直しにつながる指摘の多くは、AIの調査から出てきたものでした。一方で私がしていたのは、その根拠が判断に足りるかを確かめることと、提示された選択肢の外に進め方がないかを考えることでした。
ワークフローは、立ち止まる場面を設問やレビューの形で用意してくれましたが、そこで何を疑い、どこまで確かめるかは、自分で判断する必要がありました。
よく考えてみると、私がバグ修正の過程でAIに出した多くの指摘は、メンバーから上がってくる報告や相談を受ける時や、レビューなどで確認する観点とほとんど同じでした。AIとのやり取りも、普段の開発で行っている確認や議論の延長線上にあるのだと感じました。
AIと一緒に開発を進めていく上での一つの実践例としてご参考になれば幸いです!
Solana P-256 Syscalls: Agave and Firedancer, Edge Case by Edge Case
Originally published on xroot.dev.
On 27 October both of Solana’s independent runtimes merged the same new feature, written separately. Agave added P-256 curve operations to its syscalls at 02:38 UTC; Firedancer added its own sixteen hours later. Different authors, different languages, different arithmetic, and consensus needs them to return the same bytes for every input a program can send, including every input the specification rejects.
SIMD-0565 extends Solana’s curve syscalls to P-256, also called secp256r1: the curve behind passkeys, WebAuthn and the secure chips in phones. We wrote our own reference implementation from the curve’s published constants, generated 626 test vectors aimed at the edges of the specification, ran them through the code Agave’s syscalls call, and checked Firedancer’s tests and source for the same cases. We found no case where the two differ. We also built a small program that verifies a P-256 signature with three of the new syscalls and no modular inverse. It costs 22,760 compute units.
- * *
What SIMD-0565 adds
No new syscall names. The proposal adds two curve ids, 7 for little-endian and 0x87 for big-endian, to the four curve syscalls programs already use for curve25519 and BLS12-381: point validation, decompression, group operations (add, subtract, multiply by a scalar) and multiscalar multiplication. Points are 64 bytes, x then y, and the point at infinity is 64 zero bytes. Compressed points are 33 bytes: a 02 or 03 parity prefix, then x. Every call has a fixed price, charged before its inputs are read, whether it succeeds or not:
Validate a point — 20 CU
Decompress — 610 CU
Add or subtract — 520 CU
Multiply by a scalar — 9,000 CU
Multiscalar, n points — 9,000 + 3,800 × (n − 1)
Most points within 1.4M CU — 367
A multiscalar call with more than 512 points fails outright, before anything is charged. The SIMD’s authors set the prices on a private ten-validator cluster with 200 ms slots, so that a block packed with any one of these operations still replays within its slot.
- * *
Two implementations of one rulebook
Agave’s version, #13918, opened in July, calls solana-secp256r1 0.1.0, a pure-Rust P-256 crate Anza published in September. Firedancer’s, #12498, opened on 22 October, is C built on AWS’s s2n-bignum routines where the build has them. What the two share is everything consensus can see: the same feature-gate key, the same seven compute prices, and five test vectors. Firedancer’s syscall tests reuse the expected outputs from Agave’s, in both byte orders.
Agreement matters more than either implementation being right by its own tests. If one client returns 0 where the other returns 1, a program that branches on the result writes different account data on each, the two compute different bank hashes for the same block, and the network splits along client lines. The dangerous inputs are the ones nobody thought to try.
- * *
The edge cases, and what both clients do
These are the inputs where a reasonable implementation could choose differently. In every row the two give the same answer: Agave on our vectors through the crate its syscalls call, Firedancer in its own tests or, where its tests don’t reach, in its source.
64 zero bytes — identity, valid
x = 0, y = √b, a real point — valid
(0, 1) — rejected
x or y written as value + p — rejected
Prefix other than 02 or 03 — rejected
33 zero bytes, SEC1’s identity — rejected
P + P, or P − (−P) — 2P
P − P, or k·P + (n − k)·P — identity
0·P, or k times the identity — identity
Scalar n, n + 1 or 2²⁵⁶ − 1 — rejected
Multiscalar, 0 points — identity
Multiscalar, bad last scalar or point — rejected
Multiscalar, over 512 points — InvalidLength
Any P-256 call before activation — InvalidAttribute
“Rejected” means the call returns 1 and writes nothing; the last two rows are errors that abort the transaction. Firedancer’s tests cover the identity, off-curve and out-of-range points, the 04 prefix, both byte orders, P + P, P − P, P + (−P), adding and subtracting the identity, 0·P, k times the identity, n·P, multiscalar calls of 0, 1, 2 and 70 points including a bad scalar or point, running out of compute and the gate being off. The x = 0 point, the value-plus-p encodings, the other prefixes, n + 1, doubling through subtraction and cancellation inside a multiscalar call we confirmed in its source.
- * *
626 vectors, and three things to know before you call them
Our reference is about 160 lines of Python written from the curve constants in NIST SP 800-186 and the SIMD text, without reading either client’s arithmetic. It reproduces all five shared vectors. From it we generated 313 vectors per byte order: 73 hand-picked cases and 240 random operations. The hand-picked ones include everything in the table above, 64-point multiscalar calls whose two 32-point halves are equal or cancel (Firedancer works in batches of 32 points, so these land on its batch boundary), and calls of 367 and 512 points. Run through solana-secp256r1 0.1.0, called exactly as Agave’s syscalls call it at the merge commit, all 626 match, including all 127 rejections.
- x = 0 is on the curve. The identity’s encoding has x = 0, and so does a real point, (0, √b). Both clients treat only the exact all-zero encoding as the identity, so (0, √b) is an ordinary point, decompression of x = 0 succeeds, and (0, 1) is rejected.
- Scalars are never reduced. A scalar of n, n + 1 or 2²⁵⁶ − 1 makes the call return 1, even when the point is the identity. A SHA-256 hash is a valid scalar only if it is below n, so reduce it first: any 256-bit value is below 2n, so one comparison and one subtraction are enough.
- Little-endian is per coordinate. With curve id 7, x and y are each byte-reversed in place, and a compressed point keeps its prefix byte first with only x reversed, which is not SEC1. The generator written in the other byte order fails validation.
- * *
The one place the code differs
Read side by side, the clients take different paths in one place: an unknown curve id or group operation in validation, group operations or multiscalar multiplication. Firedancer’s syscalls always abort with InvalidAttribute. Agave aborts only when the feature from SIMD-0137 is active, and otherwise returns 1. That feature has been active on mainnet since slot 311,904,000, the first slot of epoch 722, so the result is the same there. Nothing on the P-256 path itself branches this way.
- * *
A signature check from three syscalls
The obvious use is checking passkey signatures inside a program, which today goes through the secp256r1 precompile. ECDSA verification normally needs s⁻¹ mod n, and no syscall computes it. It can be avoided. The signature is valid when s·R equals z·G + r·Q up to sign, where R is the point whose x-coordinate is r, z the message hash and Q the public key. Decompression gives R, one multiplication gives s·R, one two-point multiscalar call gives z·G + r·Q, and comparing x-coordinates ignores the sign:
use solana_define_syscall::{
curve_constants::{GROUP_OP_MUL, SECP256R1_BE},
definitions::{sol_curve_decompress, sol_curve_group_op, sol_curve_multiscalar_mul},
};
/// ECDSA check: s·R == ±(z·G + r·Q), where R is the point with x = r.
/// Same answer as the textbook check except for x(R) = r + n, which has
/// probability about 2^-128 and is not tried here.
pub fn verify_p256(q: &[u8; 64], z: &[u8; 32], r: &[u8; 32], s: &[u8; 32]) -> bool {
if is_zero(r) || is_zero(s) || *r >= N || *s >= N {
return false;
}
// The syscalls reject scalars >= n instead of reducing them.
let z = if *z >= N { sub(z, &N) } else { *z };
let mut c = [0u8; 33];
c[0] = 0x02; // either parity: s·(−R) = −(s·R) has the same x
c[1..].copy_from_slice(r);
let (mut rp, mut u, mut t) = ([0u8; 64], [0u8; 64], [0u8; 64]);
let scalars = [z, *r];
let points = [G, *q];
unsafe {
// R: 610 CU
if sol_curve_decompress(SECP256R1_BE, c.as_ptr(), rp.as_mut_ptr()) != 0 {
return false;
}
// U = s·R: 9,000 CU
if sol_curve_group_op(SECP256R1_BE, GROUP_OP_MUL, s.as_ptr(), rp.as_ptr(), u.as_mut_ptr()) != 0 {
return false;
}
// T = z·G + r·Q: 9,000 + 3,800 CU; returns 1 if Q is not a valid point
if sol_curve_multiscalar_mul(
SECP256R1_BE,
scalars.as_ptr() as *const u8,
points.as_ptr() as *const u8,
2,
t.as_mut_ptr(),
) != 0
{
return false;
}
}
t != [0u8; 64] && u[..32] == t[..32]
}
// N and G are the group order and generator, big-endian; is_zero and sub are byte helpers.
We built it with platform-tools v1.57 and ran it in solana-sbpf 0.26.0, the VM version Agave pinned at the merge, set up as in our CPI measurement, with the three curve syscalls implemented the way Agave implements them on top of the same crate. OpenSSL signed 20 messages, and the program got each signature, the same signature with the other half of S, a hash with one bit flipped, and a different public key. All 80 verdicts match textbook ECDSA:
Curve syscalls: 610 + 9,000 + 12,800 — 22,410 CU
Memory syscalls — 30 CU
Program instructions — 320 CU
One signature check — 22,760 CU
Precompile, cost-model charge — 4,800 CU
Two differences from the precompile. Cost: a check through the syscalls takes about 4.7 times the 4,800 compute units the cost model charges per precompile signature, before the message is hashed. Policy: this check accepts both halves of S, where the precompile rejects the high one, the reason half of raw WebAuthn signatures fail it. A program that wants the precompile’s rule adds s ≤ n/2; one that doesn’t can skip normalising S altogether. The SIMD gives that freedom as the reason for curve operations instead of a single verify syscall, which would have had to fix one malleability rule for everyone.
- * *
Where it stands
Both pull requests introduce the gate secp256r1_syscall_enabled, key SeRjtX7Abiq5hQYbzdCny266oveDseHZh9tnrs2cdVC, so the syscalls were off on every cluster when they merged, and a program calling them gets InvalidAttribute until the feature is activated. The SIMD itself was still marked Draft, with its compute prices added a week earlier. The program side is ready: solana-define-syscall 5.2.0, published in July, already declares the curve ids and all four syscalls, and our program imports them from there.
- * *
What this does not show
We did not build or run Firedancer. Its side rests on reading its source and on its own CI run, which passed test_secp256r1 and test_vm_syscall_curve on the pull request’s final commit, whose P-256 files are identical to the merged ones. On Agave’s side we ran the crate its syscalls call, not the syscalls: memory translation, compute charging and the gate we read rather than executed. Vectors find disagreements; they cannot prove there are none. The compute prices are the SIMD’s calibration, not our measurement, and 22,760 is one program built with one toolchain.
- * *
P-256 Syscalls: Questions People Actually Ask
What does SIMD-0565 add to Solana?
It extends the four existing curve syscalls (point validation, decompression, group operations and multiscalar multiplication) to P-256, also called secp256r1, with curve id 7 for little-endian and 0x87 for big-endian encoding. Points are 64 bytes, x then y, with 64 zero bytes for the point at infinity. Agave and Firedancer both merged an implementation on 27 October 2026, behind a new feature gate.
How much does it cost to verify a P-256 signature with the new syscalls?
Our test program verified an ECDSA P-256 signature for 22,760 compute units: 22,410 for one decompression, one scalar multiplication and one two-point multiscalar multiplication, 30 for memory syscalls and about 320 program instructions. Hashing the message is extra. The secp256r1 precompile is charged 4,800 compute units per signature in the cost model.
Do the secp256r1 syscalls reduce scalars modulo the group order?
No. A scalar of n or more makes the call return 1 in both Agave and Firedancer, including n + 1 and 2^256 − 1. A program that uses a 256-bit hash as a scalar must reduce it first; since any 256-bit value is below 2n, one comparison and one subtraction are enough.
How is the point at infinity encoded for Solana’s P-256 syscalls?
As 64 zero bytes. Only that exact encoding is the identity: (0, √b), which also has x = 0, is an ordinary point on the curve, and (0, 1) is rejected. Decompression cannot produce the identity, and 33 zero bytes are rejected.
Are the P-256 syscalls active on mainnet?
Not when they were merged. Both pull requests introduced the feature gate secp256r1_syscall_enabled, so it was inactive on every cluster, and every P-256 call fails with InvalidAttribute until it is activated.
- * *
The one-line version
Agave and Firedancer implemented SIMD-0565’s P-256 syscalls independently and agree on every edge case we could find, from the x = 0 point to unreduced scalars; with them, a program verifies a P-256 signature for 22,760 compute units and sets its own rule for S.
Sources: SIMD-0565 at commit f13f5d65; Agave #13918, merged as cf58f070; Firedancer #12498, merged as 8e7388db; solana-secp256r1 0.1.0, solana-define-syscall 5.2.0 and solana-sbpf 0.26.0. The reference, vectors, harnesses and test program are our own.
AI 写代码越写越乱,没人把关、出了事也查不到?
AWS 的 AI-DLC 管起开发全流程:5 阶段 33 步,7 个工具通用,关键节点要审批,全程留审计记录。
团队用 AI 写代码的可以看看,流程先立起来。
你的 AI 写代码有人审吗?
#AI编程 #开发流程 @grok
awslabs/aidlc-workflows
4 likes · 272 views
逆引き! Bedrock AgentCore gateway ユースケース38選
はじめに
Amazon Bedrock AgentCore Gateway は、Lambda 関数や REST API、MCP サーバーなどを、AI エージェントが MCP(Model Context Protocol)で呼び出せるツールとしてまとめて公開するマネージドサービスです。2025 年 10 月の一般提供のころはツールのための MCP ゲートウェイでしたが、2026 年に入ってモデルの呼び出しを振り分ける inference ターゲット、エージェントへの呼び出しを中継する HTTP ターゲット、レート制限、Step Functions のコネクターなどが加わり、開発者ガイドも現在は Gateway をエージェント、ツール、モデルのための AI ゲートウェイと説明しています1。ツールを持たなければAIエージェントはただの推論エンジンです。AIエージェントが本番稼働する中で、「何かを繋げる」ためのエコシステムを統合的に扱うためのサービスとして拡張されてきたと言えるでしょう。
この記事では、公式ドキュメント、AWS ブログ、AWS Japan の Zenn 記事を横断して、Gateway のユースケースをやりたいことから引けるように、8 分類 38 件に整理してみました(もちろん、ここに示す以外のユースケースも多くあるはずです。ここに示すのはあくまで一つの例にすぎません)。情報は 2026 年 11 月 4 日時点のものです。
AgentCore Gateway の構成要素
Gateway の構成要素を 1 枚にまとめると、図 1 のようになります。
図1: AgentCore Gateway の構成要素
Gateway の背後に置く接続先をターゲットと呼び、表 1 のように MCP、HTTP、inference の 3 種類があります2。
ターゲット
主な接続先
Gateway が行うこと
MCP ターゲット
Lambda、API Gateway、OpenAPI、Smithy、MCP サーバー、SaaS の統合テンプレート、Web Search などの組み込みコネクター
すべてのツールを 1 つの MCP サーバーとして公開し、有効にすればセマンティック検索のツールも加える
HTTP ターゲット
AgentCore Runtime のエージェント、A2A のエージェント、任意の HTTPS エンドポイント、AgentCore Memory のコネクター
プロトコルを変換せず、パスで宛先を選んで中継する
inference ターゲット
Amazon Bedrock、OpenAI、Anthropic、OpenAI 互換のサービス
リクエストの model の値でプロバイダーを選んで中継する
表1: ターゲットの種類
AgentCore Runtime は、エージェントや MCP サーバーをホストする AgentCore のサービスで、A2A はエージェント同士が連携するためのプロトコルです。また、MCP ターゲットのうち、既存の MCP サーバーをつなぐものを MCP サーバーターゲットと呼びます。呼び出し元は、MCP ターゲットのツールを /mcp で、HTTP ターゲットを /{ターゲット名}/ で始まるパスで3、inference ターゲットを /inference で始まるパスで呼び出します4。作成時にプロトコルの種類を MCP にした Gateway と、セマンティック検索を有効にした Gateway は MCP ターゲットしか持てないので、HTTP や inference のターゲットを使う Gateway は種類を指定せずに作ります5。
呼び出し元を確かめる Inbound 認証は、Amazon Cognito や外部の ID プロバイダー(IdP)が発行した JWT 形式のトークンか、IAM の認証情報で付けた SigV4 署名を検証します。認証だけを行う AUTHENTICATE_ONLY と、認証もしない NONE の方式もあり、この 2 つでは Policy、interceptor、ターゲットのどこかで認可します6。ターゲットを呼ぶときの Outbound 認証は、Gateway のサービスロール、OAuth、API キー、トークンのパススルーなどから、ターゲットの種類ごとに使える方式を選びます7。Inbound 認証と Outbound 認証のあいだに、ツールごとの許可を Cedar というポリシー言語で書く Policy、Lambda で判定や書き換えを挟む interceptor、呼び出し元やモデルごとのレート制限、HTTP と inference のターゲットへの振り分けとモデルのフォールバックを決める Gateway rules を組み合わせます。この記事では、どの部品を主に使うかで、ユースケースを表 2 の 8 つの分類に分けました。
分類
やりたいことの例
主に使う部品
1. 既存資産のツール化
既存の API や業務フローをエージェントに使わせたい
MCP ターゲット、組み込みコネクター
2. MCP サーバーとツールの集約
散らばった MCP サーバーとツールを 1 か所で管理したい
MCP サーバーターゲット、セマンティック検索、AWS Agent Registry
3. ツール呼び出しの統制
誰が何を呼べるかを決めて記録したい
Inbound 認証、Policy、interceptor、レート制限
4. 利用者 ID の伝播とマルチテナント
利用者本人の権限で下流を呼び、テナントを分けたい
Outbound 認証、interceptor
5. 閉域接続/ハイブリッド接続
VPC 内やオンプレミスの資産を公開せずに使わせたい
VPC egress、インターフェイス VPC エンドポイント
6. モデル呼び出しの一元管理
利用者ごとにモデルと予算を管理したい
inference ターゲット、Gateway rules、レート制限
7. エージェント呼び出しの集約
複数のエージェントへの呼び出しを 1 か所で管理したい
HTTP ターゲット、Gateway rules
8. 長時間処理と人の確認
処理の途中で利用者に確認したい、長い処理を待ちたい
elicitation、MCP Tasks、Step Functions のコネクター
表2: 8 つの分類と主に使う部品
なお、Gateway はエージェントとツールのあいだに必ず置くものではありません。AWS の設計ガイドである AWS Prescriptive Guidance は、ツールをエージェントのコードの中に持つ方法、リモートのツールに直接つなぐ方法、ツールゲートウェイを置く方法の 3 つを挙げ、統制の要件、ツールの数、運用の成熟度で選ぶよう勧めています8。実際、1 つのクライアントから AgentCore Runtime 上の 1 つの MCP サーバーを使うだけなら直結の方が構成も認証も単純で、AWS Japan の Zenn の検証では直結の方が中央値で約 0.94 秒速い結果でした9。
!
Gateway を置いても、エージェントやアプリがターゲットを直接呼べるなら、Gateway の認証や Policy は迂回されます。例えば、AgentCore runtimeにデプロイされたエージェントがAgentCore memoryにAgentCore gateway経由でアクセスできるように実装していても、runtime側でmemoryへのアクセス権限を持っているとgatewayをバイパスできる可能性があります。Runtime 側の送信元の制限や、ナレッジベースと Memory のリソースベースポリシーで Gateway 以外からの呼び出しを拒否する設定は、Gateway とセットで考えておく必要があります。
1. 既存資産のツール化
既存の API や Lambda 関数、業務フロー、SaaS、社内文書、Web 検索を、MCP サーバーを自分で作らずにツールとして使えるようにする分類です。Prescriptive Guidance は、業務ロジックが Lambda や既存の API にあるなら Gateway を先に検討し、独自の MCP サーバーが必要なときに AgentCore Runtime でホストするよう勧めています10。
既存の REST API を改修せずにツールにしたい
OpenAPI 定義を OpenAPI ターゲットに登録すると、operation ごとにツールが作られ、Gateway が MCP の呼び出しを REST の呼び出しに変換します。オリックス株式会社様の文書管理 SaaS である PATPOST は、外部に公開していた REST API を改修せずに、PoC としてこの方法で MCP サーバー化しました。顧客エンゲージメント基盤を提供する MoEngage 様は、約 400 の API operation を 10 の社内エージェントに公開していて、各チームが OpenAPI 定義に注釈を書くと、CI がツールとして登録します。MCP サーバーを 20 以上作る案に比べ、約 6 か月分の開発工数を省いたと見積もっています。ただし、API を 1 対 1 でツールにすると呼び出しの往復が増えやすくなります。
参考
OpenAPI スキーマターゲット - Amazon Bedrock AgentCore:OpenAPI ターゲットの設定項目と制約
REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み:既存の API を改修せずに MCP サーバー化した事例
How MoEngage turned over 400 APIs into agent tools with AgentCore Gateway:約 400 の API をツールにした事例
API Gateway で管理している REST API を使わせたい
API Gateway の REST API は、ステージ単位でターゲットにできます。API Gateway のコンソールからも追加でき、公開する操作はパスとメソッドの許可リストで絞り込みます。対象は同じアカウントかつ同じリージョンの REST API で、HTTP API と WebSocket API は使えません。エンドポイントタイプは public が前提なので、VPC 内のバックエンドへは、API Gateway の private integration でリクエストを転送します。
参考
ターゲットとしての Amazon API Gateway REST API ステージ - Amazon Bedrock AgentCore:対応する API の条件とツールの絞り込み
Streamline AI agent tool interactions: Connect API Gateway to AgentCore Gateway with MCP:既存の REST API をターゲットにする手順
Lambda の業務ロジックをツールにしたい
Lambda ターゲットでは、ツールのスキーマを Gateway に登録し、呼び出しのたびに Gateway が Lambda 関数を実行します。既存の関数も、Gateway が渡す入力の形式に合わせて手を入れれば使えるほか、細かい API をタスク単位にまとめ直したツールを作る場所にもなります。2025 年 11 月の AWS Japan の Zenn の検証では、AgentCore Runtime に置いた MCP サーバーより約 2 倍速く応答した一方、ツールの実装とスキーマが別々の場所に分かれ、ローカルで MCP サーバーとして試せない点が挙げられていました。
参考
AWS Lambda 関数のターゲット - Amazon Bedrock AgentCore:Lambda ターゲットとツールスキーマの書き方
Bedrock AgentCore で Remote MCP サーバーをホストする2つの方法の徹底検証:Runtime と Gateway + Lambda の比較
決まった手順で進む業務フローを呼ばせたい
2026 年 10 月に加わった Step Functions のコネクターを使うと、既存のステートマシンを、Lambda も MCP サーバーも書かずにツールにできます。エージェントが決めるのは実行のタイミングと入力だけで、手順の順序はステートマシンが決めるので、返金や各種の申請のように、決められた順序を守る必要がある業務に向いています。実行の開始や状況の確認など 10 の操作を公開できます。実行を開始する操作などは対象のステートマシンを作成時に固定でき、実行 ARN やタスクトークンを受け取る残りの操作は、サービスロールの IAM で範囲を絞ります。
参考
AWS Step Functions as Connector Target - Amazon Bedrock AgentCore:公開できる操作と、ステートマシンの固定の仕方
Step Functions のステートマシンを AgentCore Gateway 経由で MCP ツールとして呼び出してみた:コンソールで設定して呼び出すまでの手順
SaaS のデータや操作を使わせたい
Salesforce、Slack、Jira、ServiceNow、Microsoft 365 系、Zendesk など 16 のプロバイダーの統合テンプレートが用意されていて、コンソールからターゲットに追加できます。使える API は、プロバイダーが提供する API の一部です。SaaS への認証情報は、エージェントの認証情報を管理する AgentCore Identity が保管します。テンプレートが受け付ける Outbound 認証は多くが API キーで、OAuth を受け付けるのは Salesforce、Zendesk、BambooHR です。Salesforce と SAP を 1 つの Gateway につなぐサンプルも公開されています。
参考
統合プロバイダーからの組み込みテンプレートをターゲットとして - Amazon Bedrock AgentCore:テンプレートごとの対応 API と認証方式
Multi-ISV Orchestration (Salesforce + SAP):複数の SaaS を 1 つの Gateway につなぐサンプル
社内文書を検索させたい
2026 年 6 月に一般提供された Managed Knowledge Base のコネクターを使うと、取り込み、ベクトルストア、検索までをマネージドにしたナレッジベースをツールにできます。公開されるのは、検索を計画して複数回実行し、引用付きの回答を返す AgenticRetrieveStream と、1 回のハイブリッド検索を行う Retrieve です。利用者ごとに検索範囲を分けるなら、interceptor で利用者の情報を引数に入れて Policy で検証し、ナレッジベースのリソースベースポリシーで Gateway 以外からのアクセスを拒否します。
参考
コネクタターゲットとしての Amazon Bedrock マネージドナレッジベース - Amazon Bedrock AgentCore:2 つのツールの入出力と設定
Bedrock Managed Knowledge Base を MCP Tool として利用する際のテナント分離・アクセス制御まとめ:利用者ごとに検索範囲を分ける方法
Web 検索を付けたい
Web Search のコネクターは、Amazon の Web インデックスを使う検索をツールとして公開し、検索クエリは AWS の中で処理されます。料金は 1,000 クエリあたり 7 ドルで、2026 年 8 月からは東京リージョンでも使え、検索先のドメインや公開日で結果を絞り込めます。Claude Code on Bedrock のように組み込みの Web 検索を使えないクライアントに検索を足す使い方が、AWS Japan の Zenn で試されています。検索結果を使った回答を利用者に見せるときは、結果に付く出典とリンクを表示する必要があります。
参考
ウェブ検索ツール - Amazon Bedrock AgentCore:設定、絞り込み、対応リージョン
Amazon Bedrock AgentCore での Web Search を発表:発表時のブログ
Claude Code に AgentCore Gateway Web Search でインターネット検索を実装する:Claude Code から使う構成と料金の比較
2. MCP サーバーとツールの集約
エージェントやクライアントが増えると、MCP サーバーへの接続の設定がクライアントごとに散らばります。AWS ブログは、その状態では認証情報が各所に残り、ポリシーがクライアントごとにずれ、監査の記録が欠け、コストの計上先が分からなくなり、レビューを経ない連携が増えると整理しています11。
部門ごとの MCP サーバーを 1 つにまとめたい
MCP サーバーをターゲットにすると、Gateway は複数のサーバーのツール、プロンプト、リソースを 1 つの MCP サーバーとして公開します。クライアントの接続先は 1 つで済み、各サーバーへの認証情報は Gateway の側で管理します。既定の設定では Gateway は同期したツールの定義を返すので、サーバー側でツールを変えたら同期の API を実行します。一覧を毎回サーバーから取る DYNAMIC の設定もありますが、セマンティック検索や authorization code の OAuth とは併用できません。Databricks や Dynatrace のように、ベンダーが提供する MCP サーバーも同じようにつなげます。
参考
MCP サーバーターゲット - Amazon Bedrock AgentCore:MCP サーバーターゲットの設定と同期
AgentCore Gateway の MCP サーバー統合による MCP アーキテクチャの変革:チームごとの MCP サーバーをまとめる例
ツール定義によるコンテキストの圧迫を減らしたい
セマンティック検索を有効にすると、Gateway に x_amz_bedrock_agentcore_search というツールが加わり、エージェントは自然言語で必要なツールを探せます。見つかったツールだけをモデルに渡す処理は、クライアントの側で用意します。また、Policy で許可しないツールは tools/list にも出なくなります。MoEngage 様は 400 ツールの定義が 1 接続で 2 万トークンを超えると見積もり、クライアント側の絞り込みは認可にならないとして、Gateway の側で絞る構成を選びました。セマンティック検索を有効にした Gateway は MCP ターゲットしか持てず、検索の範囲もその Gateway の中に限られます。
参考
自然言語クエリを使用して AgentCore ゲートウェイ内のツールを検索する - Amazon Bedrock AgentCore:検索ツールの使い方と対応リージョン
How MoEngage turned over 400 APIs into agent tools with AgentCore Gateway:Policy で一覧を絞る構成
社員のコーディングエージェントの MCP 設定を統制したい
Kiro や Claude Code の mcp.json に、社内システムの認証情報が平文で残っている状態をなくす使い方です。Gateway の Inbound 認証を社内 IdP の SSO にし、バックエンドの認証情報は AgentCore Identity に保管し、呼び出しは CloudTrail に記録します。AWS ブログは、2 人のアナリストが Amazon Quick から使い始め、6 か月で 1,000 人に広げた金融機関を参考の導入例として、接続、統制、カタログ化、堅牢化の 4 段階で紹介しています。mcp.json は端末管理のツールで配り、Gateway 以外の MCP サーバーへの通信は社内のプロキシや EDR で止めておきます。
参考
Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:4 段階で統制を広げる構成と導入の経過
ゲートウェイのインバウンド認可を設定する - Amazon Bedrock AgentCore:JWT と IAM の Inbound 認証の設定
社内のツールを探して再利用させたい
AWS Agent Registry は、エージェント、MCP サーバー、ツール、スキルを登録し、承認を経たものだけを検索できるようにするカタログです。2026 年 8 月に一般提供され、AWS Organizations の各アカウントにある AgentCore Runtime と Gateway を自動で検出して登録できるので、組織のアカウントに作られたまま管理者が把握していない Runtime や Gateway を見つける手段にもなります。見つけたツールは自分の Gateway のターゲットに加え、Policy で呼び出しを許可して使います。
参考
AWS エージェントレジストリ: エージェント、ツール、リソースを検出して管理する - Amazon Bedrock AgentCore:Registry の機能と使い方
Kiro に野良 MCP を繋がせない。AWS Agent Registry × Kiro for Enterprise で配布を統制する:Kiro と組み合わせて配布を統制する検証
Amazon Quick から社内システムを使わせたい
業務向けの AI アシスタントである Amazon Quick の MCP 連携から Gateway につなぐと、Lambda のツールを経由して、VPC 内の基幹 DB を公開せずに業務部門のチャットから使えます。Quick は 1 つの MCP 接続に 100 件までしかツールを登録せず、サービス間の OAuth では Quick の利用者個人を Policy で識別できません。業務ドメインごとに Gateway を分け、利用者ごとの認可が必要ならユーザー認証の方式を検討します。Runtime 上の MCP サーバー 1 つを使うだけなら、Gateway を挟まない直結も選べます。
参考
Amazon Quickから基幹システムのRDSを使う!AgentCore Gatewayと関数型MCPツールで作るリファレンス構成:基幹 DB を公開せずに使う構成とツールの設計
AgentCore Gateway をどう設計するか。MCP の集約・検索・認可を Amazon Quick で検証:直結と Gateway 経由の比較
Connect an AgentCore Runtime hosted MCP server to Amazon Quick:Runtime の MCP サーバーを Quick につなぐ手順
3. ツール呼び出しの統制
誰がどのツールを呼べるかを決め、呼び出しを記録する分類です。アクセス制御の主な手段は Inbound 認証、Policy、interceptor の 3 つで、Policy がプレビューだった 2026 年 1 月の AWS Japan の Zenn の測定では、50 回の平均で、Inbound 認証だけの構成に比べて tools/list の応答が Policy で約 134 ミリ秒、interceptor で約 207 ミリ秒増えました12。測定した記事は、ツールを何度も呼ぶエージェントでは差の積み重なりを考える必要があるものの、柔軟な認可を考えれば許容できる範囲だと結論しています。
部署やロールごとに使えるツールを変えたい
Policy は、Cedar で書いたポリシーを Gateway への呼び出しごとに評価し、許可されたツールの呼び出しだけを通します。既定は拒否で、LOG_ONLY のモードで影響を確かめてから ENFORCE に切り替えられ、ポリシーの案は自然言語からも作れます。JWT のクレームで部署やグループを見分けられ、条件にはツールの引数も使えるので、デプロイのツールを staging の環境に限って許可する、といった書き方もできます。Inbound 認証だけの構成では、認証を通った呼び出し元がすべてのツールを呼べます。
参考
Amazon Bedrock AgentCore のポリシー: エージェントのインタラクションを制御する - Amazon Bedrock AgentCore:Policy の機能と使い方
AgentCore Gateway で MCP サーバー・ツールのアクセス制御を実現する 3 つの方法の徹底比較:Inbound 認証、Policy、interceptor の使い分け
外部の権限情報や複雑な条件で判定したい
interceptor は、Gateway がターゲットを呼ぶ前と後に実行される Lambda 関数で、外部の DB を参照した判定や、引数と結果の書き換えができます。Cedar のポリシーは呼び出しに含まれる情報だけで判定するので、interceptor が属性を取ってきて引数に入れ、Policy がその値で判定する組み合わせが AWS ブログで紹介されています。この例では、EU の利用者には個別の請求記録を見せないという規則を、DynamoDB に置いた利用者の地域と Cedar の forbid で実装しています。
参考
Gateway でのインターセプターの使用 - Amazon Bedrock AgentCore:interceptor の種類と設定
Secure AI agents with Policy and Lambda interceptors in Amazon Bedrock AgentCore gateway:Policy と interceptor の使い分けと組み合わせ
Amazon Bedrock AgentCore ゲートウェイインターセプター: きめ細かなアクセス制御の実現:ツール一覧の絞り込みや ID の伝え方の例
承認の後でしか実行させたくない
temporal policy は、同じセッションで過去に行われた操作を条件にできるポリシーです。承認を確かめる操作が成功していなければ送金を拒否する、ある操作の回数や累計額に上限を設ける、といった、1 回の呼び出しだけでは決められない規則を Gateway で判定できます。ここでのセッションは、呼び出し元が x-amzn-bedrock-agentcore-policy-session-id ヘッダーで渡す policy session で、回数や累計額もそのセッションの中だけで数えます。ポリシーは Cedar と互換の Dogwood で書き、LOG_ONLY のモードで試せます。
参考
一時ポリシー - Amazon Bedrock AgentCore:temporal policy の書き方
Temporal policies のサンプル:銀行のアシスタントを題材にしたサンプル
ツールの入出力から個人情報を除きたい
Policy の Guardrails 連携を使うと、ターゲットへの入力やツールの結果を Amazon Bedrock Guardrails で検査し、個人情報やプロンプトインジェクションの疑いを条件に拒否できます。検査はエージェントのコードの外で行われるので、エージェントが推論で迂回することはできません。値をマスクしたい場合は、モデルを呼ばずにガードレールの判定だけを行う ApplyGuardrail API を interceptor から呼ぶ方法があり、AWS Japan の Zenn では、マイナンバーを引数と結果の両方でマスクできることを確かめています。番号を登録するためのツールでは、引数をマスクすると番号を登録できなくなるので、結果だけをマスクするよう分ける必要がありました。
参考
ポリシーのガードレール - Amazon Bedrock AgentCore:Guardrails の判定をポリシーの条件にする方法
MCP ツールの引数と結果に入った個人番号を、AgentCore Gateway のインターセプターでマスクする:interceptor でのマスクと制約
汎用の MCP クライアントから利用者のログインで使わせたい
MCP クライアントの中には、動的クライアント登録(DCR)で自分を OAuth のクライアントとして登録してから接続するものがあります。IdP が DCR に対応していれば、その IdP を Gateway の JWT の Inbound 認証に使う構成を取れます。対応していない場合は、API Gateway と Lambda で登録を受け付けて Cognito のアプリクライアントを作り、Gateway の許可リストに加える仲介を置く構成が AWS ブログで紹介されています。DCR に対応した Auth0 を使う構成は、AWS Japan の Zenn で試されています。
参考
AgentCore Gateway + Auth0を利用して、DCR可能なリモートMCPサーバーを構築してみた:Auth0 で DCR に対応させる構成
Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:Cognito を使う DCR の仲介の構成
一部の利用者の使いすぎを抑えたい
2026 年 8 月から、Gateway に自分でレート制限を設定できるようになりました。JWT のクレームや IAM の身元、ターゲット、ツール、モデルの組み合わせごとに、1 秒あたりや 1 分あたりのリクエスト数、推論の 1 分あたりのトークン数、同時接続数の上限を置けます。複数の制限はすべてを満たす必要があります。上限を 0 にすると特定の呼び出し元を止められますが、反映に最大 30 秒かかり、判定できないときは呼び出しを通すので、確実に遮断するなら認証や Policy で拒否します。レート制限は Policy より先に評価されるので、Policy で拒否される呼び出しも回数に数えられます。追加料金はかかりません。
参考
ゲートウェイにレート制限を追加する - Amazon Bedrock AgentCore:制限の単位と設定
Configure rate limits for AI traffic on AgentCore gateway:ロールや利用者ごとの制限を組み合わせる例
誰が何を呼んだかを監査で説明したい
Gateway への呼び出しは CloudTrail のデータイベントとして記録でき、CloudWatch にはログとトレース、Policy の許可と拒否の判定が残ります。AWS ブログの金融機関の例では、どのアナリストがどのポジションを、どのポリシーの下で照会したかを規制当局がたどれる状態にしています。利用者の単位で説明するには、サービス間の認証ではなく、利用者のトークンで Gateway を呼ぶ構成にしておく必要があります。
参考
CloudTrail を使用した Amazon Bedrock AgentCore Gateway API コールのログ記録 - Amazon Bedrock AgentCore:データイベントの有効化と記録の内容
AgentCore が生成したゲートウェイオブザーバビリティデータ - Amazon Bedrock AgentCore:メトリクス、ログ、トレース
4. 利用者 ID の伝播とマルチテナント
人が画面から操作するときはアプリが利用者を把握していますが、エージェントが利用者の代わりに API を呼ぶと、いま誰が操作しているかという情報が途中で失われやすくなります13。この分類では、利用者の ID を下流の API まで渡すことと、テナント ID のようなアクセス制御に使う値を、LLM が組み立てるツールの引数にしないことの両方が求められます。
利用者本人の同意で SaaS 上のデータを扱わせたい
Outbound 認証に authorization code の OAuth を使うと、Gateway は利用者ごとのトークンで SaaS を呼び出します。利用者がブラウザで一度同意すると、トークンは AgentCore Identity が利用者ごとに保管し、以降の呼び出しに使います。MCP ターゲットでの対応は 2026 年 4 月に一般提供され、9 月には同意画面を用意する Consent Portal も AgentCore Identity に加わりました。Consent Portal を使うには、JWT で Inbound 認証をする Gateway が必要です。
参考
ゲートウェイのアウトバウンド認可を設定する - Amazon Bedrock AgentCore:ターゲットの種類ごとに使える認証方式
同意ポータルを設定する - Amazon Bedrock AgentCore:Consent Portal の設定
利用者の ID を下流の API まで渡したい
Outbound 認証の token exchange を使うと、Gateway は受け取った利用者のトークンを、下流の API 向けに範囲を絞ったトークンに交換してから呼び出します。交換後のトークンには利用者とエージェントの両方の ID が入るので、下流の API は追加の同意なしに利用者の単位で認可できます。受け取ったトークンをそのまま下流に渡すと権限の流用につながり、MCP の認可仕様もこれを禁じています。Gateway でトークンをそのまま渡す方式を選べるのは HTTP ターゲットだけで、token exchange は MCP サーバーと OpenAPI のターゲットで使えます。
参考
AgentCore Identity とのトークン交換On-behalf-of - Amazon Bedrock AgentCore:token exchange の仕組みと設定
Implement on-behalf-of token exchange for multi-tenant agents with Amazon Bedrock AgentCore Gateway:マルチテナントでの実装例
テナントごとにデータを分けたい
全テナントで同じエージェントとツールを使いながらデータを分けるには、REQUEST interceptor で ID トークンからテナントを読み取り、テナントの一時認証情報かテナント ID をツールに渡します。分離はデータベースの側で強制し、AWS Japan の Zenn では、Cognito の ID プールで交換した一時認証情報を Lake Formation で使う分離と、テナント ID で Aurora PostgreSQL のロールを切り替えて行レベルセキュリティを適用する分離を確かめています。どちらの例も、テナント ID を LLM が作る引数にしていません。
参考
AgentCore Gateway でマルチテナント環境における Agent のデータアクセス制御 (FGAC) を実現する:Lake Formation で分離する構成
AI エージェントの DB アクセスを AgentCore Gateway + PostgreSQL のネイティブ権限で制御する:PostgreSQL の権限で分離する構成
Gateway によるヘッダー伝達 - Amazon Bedrock AgentCore:ターゲットにヘッダーを渡す設定
利用者ごとの API キーで動く SaaS を使わせたい
利用者ごとに API キーを発行している既存の SaaS では、interceptor の中で利用者の API キーを付けてから OpenAPI ターゲットに渡す方法があります。オリックス株式会社様の PATPOST はこの形で、すでに使っていた Cognito と利用者ごとの API キーを活かし、本体に手を入れずに利用者ごとにデータを出し分けました。API キーがエージェントの実行環境やモデルのコンテキストに渡らないので、プロンプトインジェクションで API キーが漏れることへの対策になります。
参考
REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み:認証情報の渡し方とツールの設計の議論
Gateway でのインターセプターの使用 - Amazon Bedrock AgentCore:ヘッダーを interceptor に渡す設定
エンドユーザーに Memory を本人の分だけ使わせたい
エージェントの記憶を管理する AgentCore Memory のデータプレーンは SigV4 の認証だけですが、Memory のコネクターで Gateway の背後に置くと、エンドユーザーが OAuth のトークンで Memory を呼べるようになり、アプリに AWS の認証情報を配らずに済みます。Memory のコネクターは HTTP ターゲットで、MCP のツールとしては公開されず、Memory の API のパスで呼び出します。Memory の記録はユーザーを表す actor と、保存先の区分である namespace で分かれるので、Policy で本人の actor と namespace への呼び出しだけを許可し、Memory のリソースベースポリシーで Gateway 以外からの呼び出しを拒否します。
参考
ゲートウェイ経由で AgentCore Memory にアクセスする - Amazon Bedrock AgentCore:Memory のコネクターと認証の方式
AgentCore Memory connector と FGAC でメモリーアクセスを統制する:本人の記録だけを使わせる構成の検証
自社のツールを顧客の AWS アカウントに提供したい
AWS Builder Center の記事は、ソフトウェアを提供する事業者(ISV)が自社の MCP サーバーを複数の顧客に提供する構成をどこにgatewayを置くかで分けた 2 つ紹介しています。ISV が Gateway を持ち、顧客は PrivateLink で呼び出し、Policy でテナントごとに使えるツールを分ける構成と、顧客が自分の Gateway を持ち、ISV の MCP サーバーを VPC Lattice のリソース設定として AWS RAM で共有してもらう構成です。後者は Transit Gateway や VPC ピアリングなしで組めます。前者では、顧客の VPC 以外からの呼び出しを Gateway のリソースベースポリシーで拒否します。図は、ISV が Gateway を持つ前者の構成です。
参考
Networking Patterns for ISVs Distributing AI Agent Tools via Amazon Bedrock AgentCore Gateway:2 つの構成の手順(AWS Builder Center)。プライベート CA の記述は 2026 年 10 月より前のもの
Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:VPC Lattice と AWS RAM による接続
5. 閉域接続/ハイブリッド接続
規制のある業種や基幹システムでは、ツールを公開エンドポイントに置けない場合があります。Gateway からターゲットへ閉域でつなぐ方式は、ターゲットの種類によって異なります14。
VPC 内の MCP サーバーや API を公開せずに使わせたい
2026 年 4 月に加わった VPC egress では、MCP サーバーと OpenAPI のターゲットに private endpoint を設定し、VPC の中に置いた VPC Lattice のリソースゲートウェイを通して、EKS や ECS のサーバーにリクエストを送ります。リソースゲートウェイは、Gateway が作成して管理する方式と、自分で作る方式から選べます。10 月からは社内 CA の証明書を信頼の起点として登録でき、ALB を挟まずに接続できます。Lambda のターゲットは関数を VPC 内で実行し、API Gateway のターゲットは VPC Link で VPC 内につなぎます。Gateway から自分の VPC への通信には、1 GB あたり 0.006 ドルのデータ処理料金がかかります。
参考
ゲートウェイターゲットの Amazon Bedrock AgentCore Gateway VPC Egress を設定する - Amazon Bedrock AgentCore:private endpoint とプライベート CA の設定
Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:ターゲットの種類ごとの接続方法
オンプレミスや他クラウドのシステムを使わせたい
VPC egress の先を Direct Connect や Site-to-Site VPN につなげば、オンプレミスや他のクラウドで動く MCP サーバーや REST API もターゲットにできます。エージェントからは、ターゲットが AWS の上にあるかどうかは見えません。AWS ブログの金融機関の例では、トレーディングのシステムをオンプレミスに残したまま、Direct Connect 経由でツールとして使っています。
参考
Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:オンプレミスや他クラウドへの接続
Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:オンプレミスのシステムをツールにした例
別アカウントのツールを使わせたい
ツールを提供するアカウントで、VPC で通信を受け入れる VPC Lattice のリソースゲートウェイと、MCP サーバーを指すリソース設定を作り、AWS RAM で利用する側のアカウントに共有します。利用する側の Gateway で、自分で作る方式の private endpoint にそのリソース設定を指定すると、AgentCore がリソース設定を AgentCore の VPC Lattice のサービスネットワークに関連付けます。Gateway からのリクエストはこのサービスネットワークとリソースゲートウェイを通って VPC に入るので、インターネットを経由せず、VPC ピアリングや Transit Gateway も要りません。
参考
Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:自分で管理する VPC Lattice によるアカウント間の接続
VPC Lattice を使用して VPC 内のプライベートリソースに接続する - Amazon Bedrock AgentCore:リソースゲートウェイ、リソース設定、サービスネットワークへの関連付けの仕組み
エージェントからも閉域で Gateway を呼びたい
インターフェイス VPC エンドポイントを作ると、VPC 内のエージェントは PrivateLink 経由で Gateway を呼べます。プライベート DNS を有効にすれば、Gateway の通常の URL のまま使えます。ただし、エンドポイントを作るだけでは公開側からの呼び出しは止まらないので、VPC からに限るには Gateway のリソースベースポリシーで aws:SourceVpc などを条件にします。VPC エンドポイントポリシーで呼べる Gateway を絞れますが、ポリシーの変更が反映されるまでに最大 15 分かかります。2026 年 3 月からは、Gateway の作成や変更のための API も PrivateLink で呼べます。
参考
インターフェイス VPC エンドポイント (AWS PrivateLink) を使用して、VPC と Amazon Bedrock AgentCore リソース間のプライベート接続を作成する - Amazon Bedrock AgentCore:VPC エンドポイントの作成
Secure ingress connectivity to Amazon Bedrock AgentCore Gateway using interface VPC endpoints:VPC エンドポイントポリシーと構成の例。コントロールプレーンの PrivateLink 対応より前の記事
Amazon Bedrock AgentCore のリソースベースのポリシー - Amazon Bedrock AgentCore:aws:SourceVpc などで呼び出し元の VPC を絞る例
6. モデル呼び出しの一元管理
2026 年 6 月に inference ターゲットが加わってできた分類です15。ツールの呼び出しに使っている Inbound 認証、Policy、レート制限を、同じ Gateway でモデルの呼び出しにも使えます。
複数のプロバイダーのモデルを 1 つのエンドポイントで使い分けたい
inference ターゲットを使うと、Gateway は OpenAI や Anthropic の API と互換のエンドポイントになり、リクエストの model の値で Amazon Bedrock、OpenAI、Anthropic などに振り分けます。クライアントは OpenAI SDK や Anthropic SDK をそのまま使い、接続先と認証情報を Gateway 向けに変えればよく、プロバイダーの認証情報は Gateway の側に置けます。Amazon Bedrock、OpenAI、Anthropic は connector を選んで名前を指定するだけで追加でき、ほかのサービスは provider でエンドポイントとモデルの対応を書きます。10 月からは InvokeModel の形式でも呼べますが、ペイロードの形式は変換しないので、本文は宛先と同じ形式にします。
参考
推論ターゲット - Amazon Bedrock AgentCore:connector と provider の設定
Use the Amazon Bedrock InvokeModel API with inference targets - Amazon Bedrock AgentCore:InvokeModel の形式との変換
利用者ごとにモデルの権限、量、予算を管理したい
エージェントがモデルを繰り返し呼ぶ使い方を想定して、モデルの利用を利用者ごとに管理する構成が、AWS Japan の Zenn で公開されています。Policy で利用者ごとに使えるモデルを決め、レート制限で 1 分あたりのトークン数と同時接続数を絞り、REQUEST と RESPONSE の interceptor が DynamoDB に記録した費用を日次と月次の予算と比べます。費用は応答のあとに計上するので、予算の超過を厳密には防げませんが、予算の管理やチームごとの支出の集計が要件の中心なら、それらを主な機能にしている LiteLLM のような AI ゲートウェイも比較の候補になります。
参考
Amazon Bedrock AgentCore Gateway によるモデル利用の認証・認可と利用量・予算管理:構成とサンプルのリポジトリ
Envoy AI Gateway は Agent Router になった。モデルと MCP の通信ポリシーを Envoy に集約する:LiteLLM や Agent Router(旧 Envoy AI Gateway)との使い分け
モデルの障害やスロットリングに備えたい
2026 年 10 月から、Gateway rules にモデルのフォールバックを書けるようになりました。順番に試す代わりの宛先と、切り替える失敗の種類、最初の 1 バイトが返るまでのタイムアウトを指定しておくと、第 1 候補がスロットリングや容量不足で応答しないときに、次の候補へ自動で切り替わります。いま失敗している宛先は、既定で有効なサーキットブレーカーが避け、次の宛先に送ります。応答のストリーミングが始まった後の失敗は切り替えられずクライアントで再試行しますが、それより前の失敗については、クライアントごとに切り替えを実装しなくて済みます。
参考
Model routing and fallback - Amazon Bedrock AgentCore:フォールバックのルールの書き方
AgentCore Gateway now supports Bedrock InvokeModel APIs and model fallback:発表時の What's New
7. エージェント呼び出しの集約
2026 年 6 月に HTTP ターゲットが加わり、ツールだけでなくエージェント自体への呼び出しも Gateway で受けられるようになりました15:1。
複数のエージェントに共通の認証と監査をかけたい
AgentCore Runtime のエージェントをターゲットにすると、/sales/invocations のように先頭のターゲット名で宛先を振り分け、Gateway の認証、Policy、ログを共通で使えます。Runtime の側では、IAM の場合は aws:SourceArn の条件、JWT の場合は許可するワークロードの設定で、Gateway 以外からの呼び出しを拒否できます。また、Runtime ターゲットを置く Gateway は、プロトコルの種類を指定せずに作ります。
参考
Amazon Bedrock AgentCore ランタイムターゲット - Amazon Bedrock AgentCore:Runtime ターゲットの制約と、Gateway 以外の呼び出しの拒否
外部の A2A エージェントや独自の HTTP サービスを並べたい
HTTP passthrough のターゲットは、任意の HTTPS のエンドポイントを Gateway の背後に置き、認証、Policy、ログを共通にします。A2A のエージェントや、単独で公開したい MCP サーバー、独自の推論エンドポイントが対象です。protocolType に A2A、MCP、INFERENCE、CUSTOM のいずれかを指定し、Gateway はその種類をログと Policy の判定に使いますが、プロトコルの変換はしません。API のスキーマを渡すと Policy の判定やガードレールを適用でき、CUSTOM でガードレールを使う場合はスキーマが必須です。
参考
HTTP パススルーターゲット - Amazon Bedrock AgentCore:passthrough の設定と対応する HTTP メソッド
新しいプロンプトやエージェントを本番の利用で比べたい
エージェントの改善を支援する AgentCore の optimization の A/B テストでは、Gateway がトラフィックをセッションごとに 2 つの版へ分け、評価の機能である AgentCore Evaluations で採点して、差が統計的に有意かどうかを確かめます。プロンプトやモデルの違いなら同じ Runtime の設定の版を分け、実装の違いなら別のエンドポイントを指す 2 つのターゲットに振り分けます。AgentCore の外で動くエージェントも HTTP ターゲットにすれば比べられ、勝った版への切り替えは Gateway rules で行います。1 つの Gateway に設定できる A/B テストは 1 つです。
参考
AgentCore 最適化: レコメンデーションと A/B テストによりエージェント品質ループを改善 - Amazon Bedrock AgentCore:A/B テストの仕組み
ゲートウェイにルールを追加する - Amazon Bedrock AgentCore:ルールの評価の順序と重み付きの振り分け
8. 長時間処理と人の確認
2026 年 5 月以降、Gateway は MCP サーバーとクライアントのあいだの双方向のやり取りを中継できるようになりました15:2。処理の途中で利用者に入力や確認を求める機能である elicitation と、時間のかかる処理の結果を後から受け取る機能である MCP Tasks を使えるのは、MCP サーバーターゲットだけです。
実行の途中で利用者に確認を求めたい
elicitation は、MCP サーバーがツールの実行中に利用者へ入力や確認を求める MCP の機能で、Gateway はその要求をクライアントに中継し、回答を元のサーバーに返します。返金を実行する前の確認のようなフォームと、OAuth の同意画面へ誘導する URL の 2 つの形に対応しています。2025-11-25 版までのプロトコルでは MCP セッションとレスポンスのストリーミングを有効にする必要があり、2026-07-28 版では不要です。
参考
AgentCore ゲートウェイで誘発を使用する - Amazon Bedrock AgentCore:elicitation の前提と動き
Extending MCP support for Amazon Bedrock AgentCore Gateway:セッション、ストリーミング、elicitation の追加
15 分を超える処理をツールにしたい
Gateway の同期の呼び出しのタイムアウトは既定で 15 分で、引き上げを申請できます。2026 年 10 月に対応した MCP の Tasks の拡張では、ツールがすぐにタスクの ID を返し、クライアントが tasks/get で状態と結果を取りに行くので、バッチ処理やレポートの作成のような長い処理もツールにできます。途中で入力を求められたら tasks/update で答え、tasks/cancel で取り消しを依頼できます。プロトコルの 2026-07-28 版が前提で、JWT か IAM で認証する Gateway では、タスクは作成した呼び出し元に紐づきます。
参考
Use tasks with your AgentCore gateway - Amazon Bedrock AgentCore:Tasks の前提と動き
How AgentCore Gateway supports the MCP 2026-07-28 spec:2026-07-28 版への移行の考え方
数日かかる承認待ちを含む業務と組み合わせたい
Step Functions の Standard ワークフローは最長 1 年動き、コールバックのタスクで外部からの完了の連絡を待てます。数日かかる承認はワークフローの側で待ち、承認者への依頼と受け付けもワークフローから行います。エージェントはコネクターの StartExecution で業務を始め、結果は後のセッションで DescribeExecution を呼んで確かめます。途中の作業をエージェントが行う場合は、ワークフローがタスクトークンを SQS などでエージェントのアプリに渡し、作業を終えたエージェントが SendTaskSuccess を呼んで再開させます。どちらの場合も待つのはワークフローなので、Runtime のセッションを承認が終わるまで動かし続ける必要はありません。有効なタスクトークンがあればそのタスクを完了できるので、トークンはクレデンシャルと同じように扱います。
参考
AWS Step Functions as Connector Target - Amazon Bedrock AgentCore:コールバックで使う操作と注意点
アップデート AWS Step FunctionsのワークフローをAgentCore Gateway経由でAIエージェントのツールとして使えるようになりました!:承認の待機を Amazon SNS とタスクトークンで実装した例
Asynchronous patterns for calling Amazon Bedrock AgentCore agents in serverless pipelines:ワークフローがエージェントを非同期に呼び出し、エージェントが SendTaskSuccess で再開させる例
まとめ
この記事で取り上げた PATPOST と MoEngage 様の事例、AWS ブログの金融機関の例は、いずれも既存の API のツール化や MCP の設定の集約から始まっていますが、設計の議論では利用者ごとの認可や監査、閉域での接続が大きな論点になっていました。Gateway を使い始めるきっかけは MCP サーバーを作らずにツールを公開できることでも、置き続ける理由になるのは、誰がどの権限で何を呼べるかを 1 か所で決め、その呼び出しを記録できることの方だと考えています。
ただし、Gateway での認可と記録は、Runtime の送信元の制限やリソースベースポリシーで Gateway を通らない呼び出しを拒否するか、下流の認証情報を Gateway だけに持たせて、初めて成り立ちます。また、1 つのクライアントから 1 つの MCP サーバーを使うだけなら直結で十分なので、つなぐツールや利用者が増えた段階で Gateway を検討してみてください。
脚注
Amazon Bedrock AgentCore Gateway: エージェント、ツール、モデル用の安全な AI ゲートウェイ - Amazon Bedrock AgentCore ↩︎
Amazon Bedrock AgentCore Gateway の主な概念 - Amazon Bedrock AgentCore ↩︎
HTTP パススルーターゲット - Amazon Bedrock AgentCore ↩︎
推論コネクタターゲット - Amazon Bedrock AgentCore ↩︎
API を使用して AgentCore ゲートウェイを作成する - Amazon Bedrock AgentCore ↩︎
ゲートウェイのインバウンド認可を設定する - Amazon Bedrock AgentCore ↩︎
ゲートウェイのアウトバウンド認可を設定する - Amazon Bedrock AgentCore ↩︎
Core services: tools - AWS Prescriptive Guidance ↩︎
AgentCore Gateway をどう設計するか。MCP の集約・検索・認可を Amazon Quick で検証 ↩︎
Deployment Pattern 1 - Amazon Bedrock AgentCore - AWS Prescriptive Guidance ↩︎
Govern AI agent tool access with Amazon Bedrock AgentCore Gateway ↩︎
AgentCore Gateway で MCP サーバー・ツールのアクセス制御を実現する 3 つの方法の徹底比較 ↩︎
REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み ↩︎
Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets ↩︎
Amazon Bedrock AgentCore のリリースノート - Amazon Bedrock AgentCore ↩︎ ↩︎ ↩︎
The Most Important AI Engineering Skill Is Saying No (AI-DLC, bolts, and autonomous agents)
---
title: "The Most Important AI Engineering Skill Is Saying No (AI-DLC, bolts, and autonomous agents)"
published: true
description: "AI-DLC, bolts, and autonomous agents promise faster software delivery. But who owns the architecture when AI starts making the decisions?"
tags: ai, softwareengineering, architecture, productivity
---
AI can now propose an architecture, break requirements into tasks, write the implementation, generate tests, and prepare a deployment.
And we are increasingly building entire development methodologies around that capability.
AWS introduced the AI-Driven Development Lifecycle (AI-DLC), with concepts such as Units of Work, Bolts, Mob Elaboration, and Mob Construction. Frameworks such as specs.md bring similar ideas into tools like Claude Code.
The promise is compelling: shorter development cycles, persistent context, specialized agents, and fewer manual handoffs.
But after exploring these methodologies, I keep returning to a question that seems more important than how quickly AI can produce software:
Who is actually making the engineering decisions?
Not who approves the final pull request. Not who clicks "continue" when an agent requests permission.
Who decides whether an architectural proposal makes sense in the first place?
{% card %}
My argument: The most valuable human contribution to AI-driven software development may be the ability to reject an AI proposal, establish constraints, demand alternatives, and stop execution.
If developers lose that authority, human-in-the-loop becomes little more than a ceremonial approval process.
{% endcard %}
We are redesigning software development around AI
For years, the software industry has experimented with methodologies intended to improve how teams deliver software.
Waterfall emphasized sequential phases. Agile introduced iterative development and faster feedback. Scrum provided a framework for organizing complex work, while Kanban emphasized managing flow.
Now we have AI-native development methodologies.
AWS describes AI-DLC as a model in which AI creates plans, asks questions, proposes solutions, and executes work after human validation.
Its lifecycle has three major phases:
| Phase | AI responsibility | Human responsibility |
|---|---|---|
| Inception | Elaborate requirements and propose units | Validate business intent and scope |
| Construction | Propose designs, generate code, and test | Make technical decisions and validate results |
| Operations | Prepare infrastructure and operational changes | Provide oversight and authorize consequential actions |
The methodology also introduces Bolts, short execution cycles generally measured in hours or days rather than weeks.
The ambition is to reorganize the development lifecycle around AI's ability to generate and execute work rapidly.
{% embed aws.amazon.com %}
This is a legitimate direction to explore.
But there is a subtle difference between accelerating software development and accelerating the production of software artifacts.
An agent can produce five architectural alternatives in minutes.
It cannot guarantee that any of them is appropriate for the organization, its operational constraints, or the business problem.
That distinction becomes increasingly important as agents gain autonomy.
A Bolt is not a substitute for engineering judgment
Consider a traditional development team implementing a reservation management feature.
A developer might spend time understanding the domain, inspecting existing services, discussing edge cases, and choosing an implementation strategy.
With an AI-driven workflow, an agent may perform much of that investigation and propose an implementation plan.
The team can then approve the plan and let the agent execute it.
This sounds efficient.
But consider what happens when the agent proposes an unnecessarily complicated solution.
For example:
> Introduce a Kafka topic to propagate reservation state changes, a Redis cache for availability lookups, and a new microservice responsible for reservation lifecycle management.
It sounds like an architecture.
It even sounds like a reasonably sophisticated one.
But what if the system currently handles a few hundred reservations per day, uses a single PostgreSQL database, and has no demonstrated scaling problem?
The correct engineering decision might be:
No. We do not need any of that.
We might need one additional endpoint, a transaction, a database constraint, and two integration tests.
An AI agent can generate a technically coherent architecture that is nevertheless a poor engineering decision.
The danger is not only incorrect code.
It is unnecessary complexity delivered at extraordinary speed.
{% card %}
A successful Bolt should not be measured by how much work the agent completed.
It should be measured by whether the team delivered a correct, maintainable increment that solves the intended problem.
{% endcard %}
Human-in-the-loop is not the same as human-in-control
This distinction deserves more attention.
Many AI workflows include human checkpoints. The agent presents a plan, and a developer approves or rejects it.
That is useful, but insufficient.
Imagine the following interaction:
AI: I recommend introducing an event-driven architecture.
Developer: Approved.
AI: I will implement three services and their supporting infrastructure.
Developer: Approved.
AI: Implementation complete. All generated tests pass.
Developer: Approved.
There were three human checkpoints.
Yet we have no evidence that meaningful engineering judgment occurred at any of them.
Now consider a different interaction.
AI: I recommend introducing an event-driven architecture.
Developer: Why? What problem does it solve that our existing transactional design cannot?
AI: It could improve decoupling and asynchronous processing.
Developer: Show me three alternatives. Compare operational cost, consistency guarantees, failure recovery, and migration effort. Our team maintains this platform with limited infrastructure capacity.
AI: A transactional implementation appears sufficient under those constraints.
Developer: Proceed with that approach. Do not introduce new infrastructure.
The second interaction may produce less code.
It may also produce substantially better software.
The developer was not merely validating output.
The developer was actively shaping the solution.
Human-in-the-loop describes participation. Human-in-control describes authority.
They are not equivalent.
The four powers developers must retain
I believe every serious AI engineering workflow should explicitly preserve four human capabilities.
1. Reject a proposal
Developers must be able to reject a design without having to provide a replacement implementation immediately.
This matters because rejecting a bad abstraction is often easier than constructing a better one.
An AI-generated design may be logically consistent but violate established architectural boundaries, introduce unnecessary dependencies, or optimize for a problem that does not exist.
A useful rejection should identify the concern:
> Reject this proposal. It introduces a new service without demonstrating a need for independent scaling, deployment, or domain ownership.
That is an engineering decision.
Not an obstacle to automation.
2. Establish constraints
AI agents need boundaries.
Not just coding conventions, but architectural and operational constraints.
For example:
architecture:
new_microservices: prohibited
database: postgresql
messaging: existing_kafka_topics_only
public_api_breaking_changes: prohibited
reliability:
retries_must_be_bounded: true
idempotency_required_for_commands: true
backward_compatibility: required
delivery:
unrelated_refactoring: prohibited
integration_tests: required
production_deployment: manual_approval
This is an illustrative policy, not a native specs.md configuration file.
The point is that constraints should be explicit, versioned, and testable wherever possible.
A developer should not need to repeat the same architectural principles every time a new agent session begins.
3. Demand alternatives
One of the most dangerous properties of AI-generated solutions is how persuasive they can appear.
A complete design document with diagrams, tradeoff tables, and confident explanations can create the impression that the design space has already been explored.
It may not have been.
For consequential decisions, developers should ask:
- What is the simplest viable alternative?
- What can we implement using existing components?
- Which assumptions would make this design inappropriate?
- What are the operational and maintenance costs?
- What would make us reverse this decision six months from now?
These questions force the agent to explore the decision rather than simply defend its first proposal.
4. Stop execution
Autonomy should have boundaries.
An agent should stop when it encounters an unresolved architectural decision, a destructive operation, a security-sensitive change, or a conflict between requirements.
Consider a database migration.
The agent discovers that implementing a new feature requires modifying a heavily used table.
It should not automatically choose a destructive migration merely because doing so would complete the task.
It should surface the conflict.
EXECUTION PAUSED
Reason:
The proposed schema change may break
existing consumers.
Options:
A. Backward-compatible additive migration
B. Versioned API and staged migration
C. Redesign the feature to avoid the change
Decision required:
Authorized engineering owner
Stopping is not failure.
Stopping at the correct moment is a feature of a well-governed autonomous system.
But who should make these decisions?
Not every decision belongs to the same person.
Product owners and business stakeholders should determine which outcomes matter.
Domain experts should validate business rules.
Developers and technical leads should own engineering decisions, including architecture, implementation constraints, reliability, and maintainability.
Authorized operational owners should control production releases.
AI can help each group evaluate alternatives and execute approved work.
It should not silently inherit their authority.
This is also where AI-DLC should be evaluated against established engineering practices.
The Scrum Guide makes developers accountable for creating the Sprint Backlog, adapting their plan, and maintaining quality through the Definition of Done.
AI-DLC changes the mechanics of planning and implementation, but it should not erase that accountability.
There is a difference between delegating execution and delegating responsibility.
What happens when agents start making architecture decisions?
Imagine a team maintaining a mature backend platform.
The system has accumulated years of architectural decisions, operational lessons, domain-specific exceptions, and integration constraints.
An AI agent receives a request:
> Implement real-time reporting for active alerts.
The agent examines the existing implementation and proposes a separate Redis-backed endpoint for active alerts, while historical alerts remain in PostgreSQL.
At first glance, the design may appear reasonable.
But the engineering team knows that this introduces two sources of truth for reporting.
Now every consumer needs to understand which endpoint to query, how to reconcile results, and what happens during synchronization failures.
The developer may instead insist on a unified reporting model with explicitly defined state transitions.
That decision requires understanding more than the immediate feature.
It requires understanding the system's history, consistency requirements, and future maintenance burden.
AI can help investigate those concerns.
But it cannot automatically determine which organizational tradeoffs are acceptable.
This is precisely where experienced engineering judgment becomes valuable.
The problem with measuring AI development through speed
AI-DLC introduces shorter execution cycles, and frameworks such as specs.md provide structured workflows for implementing them.
But how should we evaluate whether these methodologies work?
A common demonstration shows an agent producing a working application in a few hours or days.
That is impressive.
It is not sufficient evidence of long-term engineering effectiveness.
I would be more interested in what happens after six months.
Can another developer understand the architecture?
Can the team safely introduce a new feature?
Do the specifications still describe the implementation?
Can a developer challenge an earlier decision without rebuilding half the platform?
How much human effort is required to review and repair generated changes?
And when something fails in production, can the team diagnose it without depending on the original agent's conversation history?
Those questions are harder to demonstrate than a successful prototype.
But they are closer to what professional software engineering actually requires.
A useful evaluation should compare:
| Metric | What it reveals |
|---|---|
| Acceptance-test success | Whether the implementation meets requirements |
| Human review time | How much effort correctness requires |
| Rework after implementation | Whether the initial solution was appropriate |
| Architectural complexity | Whether unnecessary components were introduced |
| Operational incidents | Whether the system remains reliable |
| Cost per accepted change | Whether faster generation creates real efficiency |
| Maintainability over time | Whether future engineers can safely evolve the system |
Counting completed Bolts is not enough.
Counting generated lines of code is even less useful.
The meaningful unit of progress is not AI activity. It is validated software value.
Are we accidentally reinventing Waterfall?
There is another uncomfortable possibility.
Spec-driven AI development often emphasizes generating requirements, architecture documents, implementation plans, tasks, and tests before or during execution.
These artifacts can improve traceability and preserve context.
But if teams begin treating AI-generated specifications as authoritative simply because they were approved, we risk recreating a familiar problem.
The process becomes:
1. Generate extensive requirements.
2. Approve the requirements.
3. Generate the architecture.
4. Approve the architecture.
5. Generate the implementation.
6. Discover that the original assumptions were wrong.
That is not automatically Agile because the implementation happened in short Bolts.
Agility requires the ability to revise assumptions and respond to new information.
A Bolt can be short while the overall process remains rigid.
Conversely, a team can use AI-generated specifications effectively if those specifications remain open to challenge, revision, and even deletion.
The value of a specification is not that it exists.
It is that it helps the team make and revisit decisions.
A better AI engineering workflow
I would rather see teams organize AI-assisted development around explicit decision boundaries.
Not every small change needs a complete architectural review.
But not every change should run on autopilot either.
Here is a practical model.
| Change | AI autonomy | Human involvement |
|---|---|---|
| Documentation correction | High | Review output |
| Small API enhancement | High within constraints | Validate contract and tests |
| Internal refactoring | Moderate | Review behavior preservation |
| Cross-service integration | Limited | Approve design and failure handling |
| Data migration | Restricted | Approve migration and rollback |
| Security-sensitive change | Restricted | Independent security review |
| Production deployment | Policy-controlled | Authorized release decision |
The important property is that autonomy is proportional to risk.
An agent should not require permission to rename a local variable.
It should not be allowed to invent a new distributed architecture without engineering approval.
{% details An example: an architecture decision prompt for an AI agent %}
Before implementing this feature:
1. Identify the affected domain boundaries.
2. Propose at least two viable solutions,
including one that reuses existing infrastructure.
3. Compare:
- Complexity
- Reliability
- Operational cost
- Backward compatibility
- Migration effort
- Failure recovery
4. Explicitly identify assumptions and unknowns.
5. Recommend the simplest solution satisfying
the documented requirements.
6. Do not implement until the architectural
decision is approved.
7. If new information invalidates the approved
design, stop and request a new decision.
This is not a replacement for engineering expertise.
It is a way to make that expertise visible in the workflow.
{% enddetails %}
The developer's role is changing, but not disappearing
I do not think the future of software engineering is developers manually writing every line of code.
Nor do I think it is developers approving whatever an autonomous agent produces.
The interesting future lies between those extremes.
Developers may spend less time producing routine implementations and more time defining constraints, exploring tradeoffs, validating behavior, and managing complexity.
But there is a danger in describing this transformation as developers becoming merely supervisors of AI agents.
Supervision without sufficient technical understanding is fragile.
If developers stop exercising implementation and architectural judgment, their ability to challenge increasingly sophisticated AI proposals may deteriorate.
We could end up with systems that are easier to generate than to understand.
And organizations that can produce software faster than they can responsibly maintain it.
That would not be an improvement in software engineering.
It would be an improvement in software production at the expense of engineering control.
The real test of AI-DLC
I am interested in AI-DLC, Bolts, and spec-driven development because they attempt to solve real problems: lost context, slow handoffs, inconsistent implementation, and repetitive work.
I also think they deserve more rigorous evaluation than demonstrations of rapid application generation.
The most convincing evidence would be a substantial production platform developed and maintained through many AI-driven iterations, with its architectural decisions, specification changes, failures, and operational history available for examination.
Not just the first successful release.
The tenth feature.
The unexpected migration.
The production incident.
The architectural decision that turned out to be wrong.
And the developer who recognized the mistake before an autonomous agent made it worse.
{% card %}
The goal should not be to remove developers from the decision-making process.
It should be to give developers better information, more alternatives, and greater leverage over the decisions that matter.
An AI engineering methodology succeeds when it improves engineering judgment—not when it makes that judgment optional.
{% endcard %}
Final thought
AI can propose a solution in seconds.
It can generate thousands of lines of code, construct tests, document decisions, and execute an entire development workflow.
But engineering has never been only about producing implementations.
It is also about knowing when an implementation should not exist.
When an abstraction is unnecessary.
When a dependency introduces more risk than value.
When a requirement is incomplete.
When a system should remain simple.
And when execution must stop.
The most important AI engineering skill may not be writing better prompts.
It may be knowing when to say no.
---
Further reading
- AWS — AI-Driven Development Life Cycle: Reimagining Software Engineering
- AWS — Building with AI-DLC using Amazon Q Developer
- AWS Labs — AI-DLC Workflows
- specs.md — AI-native development workflows
- The Scrum Guide (2020)
What do you think? Are AI-native development methodologies giving engineers more control over software design, or gradually turning them into reviewers of decisions made by agents?
1 reaction
AIエージェントが開発中のWebアプリをリアルタイムプレビューできるBedrock AgentCore Session endpointsを試してみた
:::note warn
この記事はAI(Codex)と一緒に作成しました。
内容は2026年11月時点の検証結果です。
:::
はじめに
2026年10月30日、AWSから以下の記事が公開されました。
> Preview web apps live as your coding agent builds them with Amazon Bedrock AgentCore runtime
AIエージェントがWebアプリを開発している最中に、
そのWebアプリをブラウザからリアルタイムにプレビューできるという内容です。
コーディングエージェントを使えば簡単にWebアプリを作れる時代になりましたが、
クラウド上で動くAIエージェントが作ったものを確認するには、
コードを取得してローカルで起動したりする手間がかかることもあります。
そこでAmazon Bedrock AgentCore RuntimeのSession endpointsという機能を使うと、
AIエージェントが動いている実行環境内のWebサーバーに、認証付きでアクセスできます。
つまり、AIエージェントがコードを編集した瞬間、その変更をブラウザで確認できるわけです。面白そうなので触ってみました。
!{348A9BEA-EF0D-4DCD-B86C-DBEFAC81422B}.png
> 赤枠がエージェントとのチャット画面、青枠がWebアプリのプレビュー画面です。
> 赤枠で相談した内容が青枠に即時反映されます。
今回はAWS公式サンプルを使って、日本向けの推論プロファイルで実際に動作を検証してみました。
実際に動かしてみた
今回やってみたことは、次の3つです。
1. AgentCore Runtime上でコーディングエージェントを動かす
2. エージェントが編集しているWebサイトをブラウザから表示する
3. 日本語で変更を指示し、Webサイトがリアルタイムに更新されることを確認する
実際には、コーヒーショップのサンプルWebサイトに対して、次の指示を送りました。
> Webサイト全体を日本語に翻訳してください。
> デザインやレイアウトは変更しないでください。
結果として、AIエージェントがindex.htmlとsrc/main.jsを編集し、
変更後のWebサイトをブラウザから確認できました。(翻訳は微妙だけど)
■変更前(英文)
!image.png
■変更後(日本語訳)
!image.png
セッション分離や認証なしアクセス、WebSocketの更新通知も気になったので、追加で検証してみました。モデルIDやRuntime更新で地味にハマったので、その対処もついでに紹介します。
Session endpointsとは?
Session endpointsは、AgentCore Runtime内で動いている
HTTPサーバーなどにアクセスできる仕組みです。
例えば、AIエージェントが同じ実行環境内でWebアプリの開発サーバーを起動していれば、
そのWebアプリを外部からプレビューできます。
通常のRuntime呼び出しとの違い
AgentCore Runtimeには、AIエージェントを呼び出すための仕組みがもともとあります。
一方、Session endpointsは、AIエージェント自身ではなく、エージェントと同じセッション内で動いている別のアプリケーションにアクセスするための機能です。
| 項目 | Runtimeの通常呼び出し | Session endpoints |
|---|---|---|
| 主な用途 | AIエージェントの実行 | セッション内のアプリへのアクセス |
| アクセス対象 | エージェントのAPI | 許可したポートのWebサーバーなど |
| 今回の用途 | Webサイトの編集を指示 | 編集中のWebサイトを表示 |
| 通信 | Runtime API | HTTP、WebSocketなど |
Session endpointsでは、あらかじめ許可したポートだけにアクセスできます。
また、URLを知っているだけで誰でもアクセスできるわけではなく、IAMなどによる認証が必要です。
今回の構成
flowchart TD
A["ローカルPCのブラウザ"] --> B["PreviewApp"]
B -->|"編集指示"| C["AgentCore Runtime API"]
C --> D["AIエージェント"]
D --> E["Amazon Bedrock<br/>Claude Haiku 5.5"]
D --> F["Webサイトのファイルを編集"]
F --> G["Vite開発サーバー<br/>Port 8090"]
G -->|"Session endpoints<br/>HTTP / WebSocket"| B
B -->|"プレビューを表示"| A
今回のサンプルでは、主に3つのポートを使用します。
| ポート | 用途 |
|---|---|
| 8080 | AIエージェントの呼び出し |
| 8090 | ViteによるWebサイトのプレビュー |
| 8081 | ステータス確認用API |
Viteはファイルの変更を検知してブラウザの表示を更新してくれるツールです。
今回の構成では、ViteのWebSocket通信もSession endpoints経由で転送されるので、
ブラウザを手動更新せずに変更内容を確認できます。
検証方法
AWS CLIの認証設定が完了していることを前提とします。
また、RuntimeやIAMロール、S3リソースを作成する権限に加え、
Runtimeの呼び出しとSession endpointsへのアクセス権限が必要です。
公式サンプルのPython依存関係は、bedrock-agentcore>=1.25.2、boto3>=1.43.122でした。
手順① 公式サンプルを取得する
まず、AWS公式のGitHubリポジトリを取得します。
(WSLのターミナルから実行しました。)
git clone https://github.com/awslabs/agentcore-samples.git
今回使用するディレクトリに移動します。
cd agentcore-samples/01-features/02-host-your-agent/01-runtime/03-advanced/16-session-endpoints
このディレクトリには、以下のようなファイルがあります。
| ファイル | 役割 |
|---|---|
| deploy.py | AgentCore Runtimeのデプロイ |
| cleanup.py | 作成したAWSリソースの削除 |
| preview_app.py | ローカルのプレビュー画面の起動 |
| session_endpoints_preview.ipynb | 公式の検証用Notebook |
| agent/app.ts | AIエージェントの実装 |
| site/ | サンプルWebサイト |
手順② Python環境を準備する
Pythonの仮想環境を作成します。
uv venv
仮想環境を有効化します。
source .venv/bin/activate
依存パッケージをインストールします。
uv pip install -r requirements.txt
これでPython側の準備は完了です。
手順③ 利用するモデルIDを確認する
公式サンプルでは、モデルIDのデフォルト値として
us.anthropic.claude-haiku-5-5が指定されています。
私は日本向け推論プロファイルを利用したかったので以下のモデルIDを使いました。
jp.anthropic.claude-haiku-5-5
なお、モデルIDと推論プロファイルのIDは別物で、一覧に表示されるモデルでも直接指定して呼べるとは限りません。利用できるモデルや推論プロファイルはリージョンやアカウントの権限によって変わるので、上記のIDがどの環境でもそのまま使えるとは限らない点だけ補足しておきます。
手順④ AgentCore Runtimeをデプロイする
公式サンプルのdeploy.pyを実行します。
日本向け推論プロファイル利用時はモデルIDを明示的に指定しましょう。
python deploy.py \
--model jp.anthropic.claude-haiku-5-5
デプロイが完了すると、RuntimeのステータスがREADYになります。
今回のサンプルは、Node.jsのアプリケーションをZIP形式でS3にアップロードし、
AgentCore Runtimeで実行する構成です。
(つまりDockerイメージやECRリポジトリを自分で準備する必要がないわけですね!)
Session endpoints用のポート設定
deploy.pyでは、次のような設定が使われています。
protocolConfiguration={
"serverProtocol": "HTTP",
"port": 8080,
"path": "/invocations"
},
allowedPorts=[
{"port": 8090},
{"port": 8081}
]
- protocolConfigurationは、AIエージェント本体の呼び出し先を設定しています。
- allowedPortsは、Session endpoints経由でアクセスを許可するポートです。
今回はWebサイトのプレビュー用に8090、ステータス確認用に8081を指定しています。
(allowedPortsを設定するだけでWebサーバーが起動するわけではありません)
手順⑤ プレビュー画面を起動する
Runtimeのデプロイが完了したら、プレビュー画面を起動します。
今回は次のPythonコードを実行しました。
python - <<'PY'
import time
from preview_app import PreviewApp
app = PreviewApp().start()
print(f"プレビューURL: {app.url}", flush=True)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
app.stop()
print("セッションを終了しました")
PY
成功すると、プレビューURLが表示されます。
プレビューURL: http://localhost:8087
ブラウザからこのURLを開きます。
すると最初にお見せした、
左側にAIエージェントとのチャット画面、右側にコーヒーショップのサンプルWebサイト
が表示されます。
!image.png
右側のWebサイトはローカルPC上で動いているわけではなく、
AgentCore Runtimeのセッション内で動いていて、Session endpointsを通じて
ローカルブラウザに表示されています。
内部では何が起きているのか?
PreviewAppの起動処理を見てみると、こうなっています。
answer = self.invoke({"prompt": ""})
started = json.loads(answer["response"].read())
self.endpoint = self.client.session_endpoint_client(
session_endpoint_url(answer)
)
self.local = self.endpoint.open_local_endpoint(
port=self.config["preview_port"],
local_port=self.proxy_port
).start()
まずInvokeAgentRuntimeでセッションを起動し、
レスポンスからSession endpointのURLを取得して、ローカル側に接続口を作っています。
このローカル側の接続口がAWS認証情報を付けてリクエストを転送してくれるので、
ブラウザ自身がAWSのSigV4署名を実装する必要はありません。地味に便利な仕組みです。
手順⑥ AIエージェントに日本語で変更を指示する
いよいよWebサイトを変更してみます。
左側のチャット欄に、以下の指示を入力しました。
!{E42D5922-5C0B-4331-B6B9-9410A8750ACA}.png
するとコードを触っているログが表示され、
最終的には、変更が完了したことを示すメッセージが表示されました。
実際に、Webサイト内の英語が日本語に変更され、ブラウザ側にも反映されました。
!{CEE37FE4-6060-4EFC-9117-6FF7F0CD7AC5}.png
AIエージェントがコードを編集し、
Webサイトの変更内容をちゃんと確認できるところまで成功しました。
なぜリアルタイムに反映されるのか?
流れはこんな感じです。
flowchart TD
A["ユーザーが変更を指示"] --> B["AIエージェントがコードを編集"]
B --> C["ファイルを保存"]
C --> D["Viteが変更を検知"]
D --> E["WebSocketでブラウザに通知"]
E --> F["プレビュー画面が更新"]
Session endpointsは、通常のHTTPリクエストだけでなく
WebSocket通信も転送できるので、Viteのホットリロード機能をそのまま使えます。
エージェントがファイルを触るたびに、Webサイトの変更をブラウザ側で確認できる、というわけです。
(実際のWebSocket通信については、後述の「追加で気になったことを検証」で詳しく紹介します。)
手順⑦ 後片付けをする
プレビューを実行しているターミナルでCtrl + Cを押し、ローカルのプレビューとプロキシを停止します。
続いて、公式サンプルに含まれるクリーンアップスクリプトを実行します。
python cleanup.py
このスクリプトで、サンプルが作ったRuntime、S3上のコード、IAMロールなどが削除されます。
ただしS3バケット自体やCloudWatch Logsなどが残る場合があるので、他の用途で使っていないか確認してから整理してください。
追加で気になったことを検証
公式サンプルを動かしてみると、認証やセッション分離、WebSocket通信など、内部の仕組みも気になってきました。そこで、いくつか追加で検証してみました。
①認証なしでアクセスするとHTTP 403が返る
AWS側のSession endpoint URLに、認証署名なしのcurlでアクセスしてみました。
read -r -p "Session endpoint URL: " ENDPOINT_URL
curl -sS -o /dev/null \
-w 'HTTP Status: %{http_code}\n' \
--max-time 15 \
"${ENDPOINT_URL%/}/"
HTTP Status: 403
ちゃんと403で弾かれました。一方、AWS SDKを使うPreviewApp経由ではアクセスできています。
(http://localhost:8088はローカルのPreviewAppなので、こっちが開けるのは当然です。Session endpoint自体が匿名公開されているわけではありません。403の詳細理由やIAMロール違いまでは追っていませんが、「認証が必要」という説明どおりの挙動でした。)
②複数セッションはちゃんと独立していた
同じAgentCore Runtimeで2つのセッションを同時起動し、変更が混ざらないか確認しました。
ターミナルAとBで、それぞれ以下のコードを実行します。事前に同じサンプルディレクトリへ移動し、仮想環境を有効化しておきます。
ターミナルA:
python - <<'PY'
import time
from preview_app import PreviewApp
app = PreviewApp(ui_port=8087, proxy_port=8091).start()
try:
print(app.session_id, app.url, flush=True)
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
app.stop()
PY
ターミナルB:
python - <<'PY'
import time
from preview_app import PreviewApp
app = PreviewApp(ui_port=8088, proxy_port=8092).start()
try:
print(app.session_id, app.url, flush=True)
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
finally:
app.stop()
PY
ブラウザでhttp://localhost:8087とhttp://localhost:8088を開き、それぞれ次の指示を送ります。
- Session A:Webサイトの背景色を赤色に変更
- Session B:Webサイトの背景色を青色に変更
!image.png
結果、Session Aは赤、Session Bは青になり、お互いの変更は混ざりませんでした。同じRuntime上でもセッションごとに異なる編集結果が保持されていました(ファイルシステムの内部的な隔離方式や、IAMユーザー間の権限周りまでは今回は検証していません)。
③WebSocketでViteの更新通知がちゃんと転送された
Session endpointsが本当にWebSocket通信も転送してくれるのか気になったので、開発サーバーをブラウザで開いてEdgeの開発者ツールで「ネットワーク」→「Socket」を見てみました。
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Protocol: vite-hmr
101 Switching ProtocolsはWebSocketへの切り替え成功を示すレスポンスで、vite-hmrはViteのホットリロードのサブプロトコルです。ちゃんとつながっていますね。
「メッセージ」タブを開いた状態で、AIエージェントに「Webサイトのメインタイトルの文字色だけを緑色に変更してください」と指示してみると、まず接続完了のメッセージが来て、
!image.png
その後、こんな更新通知が届きました(主要項目を抜粋)。
{
"type": "update",
"updates": [
{
"type": "css-update",
"path": "/src/style.css?direct",
"acceptedPath": "/src/style.css?direct",
"timestamp": 1793593126711
}
]
}
css-updateは、ViteがCSSの変更を検知してブラウザへ送る更新通知です(ページ全体を再読み込みするfull-reloadとは別物)。
flowchart TD
A["ユーザーが日本語で変更を指示"] --> B["AIエージェントがCSSを編集"]
B --> C["Viteが変更を検知"]
C --> D["WebSocketでcss-updateを送信"]
D --> E["Session endpointsが通信を転送"]
E --> F["ローカルプロキシ経由でブラウザが受信"]
ファイル変更の検知と更新通知はVite側の仕事で、Session endpointsは
そのWebSocket通信を転送する役割を担っています。そのおかげでAIがCSSを編集した直後に
ブラウザ側へちゃんと通知が届くところまで確認できました
(今回確認できたのはCSSの更新だけなので、HTMLやJSの変更も必ず再読み込みなしで反映される、と断定はできません)。
④プレビューを止めるとどうなるか
PreviewAppをCtrl + Cで止めると、ローカルのプレビュー画面にはアクセスできなくなりました。新しいセッションを起動しても、前の編集内容は引き継がれません。
(あくまでローカルのPreviewAppとプロキシを止めた結果の観察です。AWS側のセッションがその瞬間に終了したかどうかまでは確認していません。)
ハマったポイント
モデルIDを後から変更するとSession endpointが取得できなくなった
手順どおりに進めるだけなら踏まなくて済む話ですが、
せっかくハマったので記録しておきます。
今回、モデルIDを変更する際に地味にハマりました。
最初はデフォルトのモデルIDでデプロイしてしまったので、
Runtimeの環境変数を後から更新しました。モデルIDの変更自体は無事成功しました。
{
"Status": "READY",
"Environment": {
"MODEL_ID": "jp.anthropic.claude-haiku-5-5"
}
}
ところが、その後プレビューを起動すると、次のエラーが出ました。
AttributeError:
'NoneType' object has no attribute 'rstrip'
エラーが発生した箇所はこちら。
self.endpoint = self.client.session_endpoint_client(
session_endpoint_url(answer)
)
つまり、Session endpointのURLが取れていなかったわけです。
調べてみると、
今回のRuntime更新処理では既存の設定を取得して更新APIにそのまま渡していたのですが、
get_agent_runtime()のレスポンスにはallowedPortsが含まれておらず、
更新リクエストでもallowedPortsを指定していませんでした。
更新のタイミングでSession endpoints用のポート設定が引き継がれずに消えてしまうのかもしれません(勘ですがallowedPortsを明示して再更新したら直ったので…)。
Boto3からupdate_agent_runtime()を呼び出し、必要なポートを明示的に指定したところ、解消しました。
import boto3
client = boto3.client(
"bedrock-agentcore-control",
region_name="ap-northeast-1"
)
runtime_id = "YOUR_RUNTIME_ID"
current = client.get_agent_runtime(
agentRuntimeId=runtime_id
)
params = {
"agentRuntimeId": runtime_id,
"agentRuntimeArtifact": current["agentRuntimeArtifact"],
"roleArn": current["roleArn"],
"networkConfiguration": current["networkConfiguration"],
"protocolConfiguration": current["protocolConfiguration"],
"lifecycleConfiguration": current["lifecycleConfiguration"],
"environmentVariables": {
**current.get("environmentVariables", {}),
"MODEL_ID": "jp.anthropic.claude-haiku-5-5"
},
"allowedPorts": [
{"port": 8090},
{"port": 8081}
]
}
result = client.update_agent_runtime(**params)
print(result["status"])
これでRuntimeが新しいバージョンに更新され、
再度PreviewAppを起動したら正常にSession endpointsが使えるようになりました。
使いどころと注意点
嬉しかったポイント
今回触ってみていいなと感じたのは、
1. AIエージェントが作ったWebアプリの変更を、その実行環境のまま確認できる
1. AIエージェントとWebアプリ(成果物)を同期する仕組みを別途用意しなくてよい
1. Webサイトに限らず管理画面や開発用APIなどSession endpointsが使える対象なら同じ仕組みを応用できる
1. セッション単位でアクセス先が分離されるため複数ユーザーが同時に使う構成にも展開できそう
使う際の注意点
今回触ってみて気をつけたいと感じたのは、
1. Session endpointsはあくまで開発中の確認用で、Webサイトを一般公開するホスティングとは別物という点(公開するならAmplify Hostingなどが必要)
1. AIエージェントが編集したファイルはRuntimeのセッション内にあるため、セッション終了や実行環境の置き換えで失われうる点(長期保存したいならS3などへの保存を検討)
1. AgentCore RuntimeやBedrockの呼び出しには料金が発生するため、検証後は不要なRuntimeやリソースを削除する必要がある
まとめ
今回は、Amazon Bedrock AgentCore RuntimeのSession endpointsを使って、
AIエージェントが開発しているWebアプリをリアルタイムにプレビューする機能を触ってみました。
変更指示、コード編集、ブラウザへ反映までを一連の流れで確認できました。
AIエージェントはコードの生成だけでなく、構築・実行・テストへと担当領域を
どんどん広げているので、こういう仕組みは重要になっていきそうです。
個人的には、PoC環境の構築に使えそうだなと感じました。
参考資料
- AWS公式ブログ:Preview web apps live as your coding agent builds them with Amazon Bedrock AgentCore runtime
- AWS公式GitHub:agentcore-samples
- 今回使用したサンプル:16-session-endpoints
- Amazon Bedrock AgentCore ドキュメント
1 like
> /aidlc-session-cost and /aidlc-replay print to the terminal and write nothing; /aidlc-outcomes-pack is the only one that writes a file (OUTCOMES.md at the workspace root).
github.com/awslabs/aidlc-workflows/blob/main/docs…
3 likes · 96 views
SOCI lazy loading: start Lambda containers faster
SOCI lazy loading lets a Lambda function on Lambda Managed Instances start before its container image has been fully downloaded. AWS announced it on October 28, 2026, and quotes startup times that are up to 70 percent shorter. I care less about that percentage than about two questions: who actually benefits from the extra build step, and where does it cause you trouble?
Container images are often the escape hatch on Lambda for anything that does not fit into a 250 MB ZIP package: ML libraries, models, PDF tools, entire runtimes. Those images quickly grow to several gigabytes. In this article, we build an example from scratch together: a small book recommendation service for a library, an image with four language models, the SOCI index, a CDK stack with a capacity provider and a script that lets you measure the effect yourself.
> In short
>
> SOCI places a table of contents next to your container image, so Lambda on Managed Instances only fetches the files your function actually reads. It pays off most for large images where each function needs only part of the content. The biggest pitfall is the format: Lambda accepts only a SOCI index v2 inside an image index with exactly two entries, and only single-architecture images.
What you will learn in this article
- What Lambda Managed Instances are and when they fit your workload
- How SOCI lazy loading fetches files on demand
- How to create a SOCI index with nerdctl and push it to ECR
- How to set up a capacity provider and function with AWS CDK in TypeScript
- How to measure startup times with and without SOCI yourself
What are Lambda Managed Instances?
Lambda Managed Instances (LMI for short) are a second way to run Lambda functions. Instead of the shared Lambda fleet, your functions run on EC2 instances in your own account. Lambda still takes care of launching, patching, routing and scaling those instances. You only decide where they may run and on which hardware.
The central building block is the capacity provider. It describes the VPC, subnets, security groups, architecture, allowed instance types and an upper limit for vCPUs. You then attach a function to that provider and publish a version. Only when you publish does Lambda launch instances, three by default for spreading across Availability Zones, plus three execution environments, before the version counts as ACTIVE.
How it differs from regular Lambda
| | Lambda (default) | Lambda Managed Instances |
| --- | --- | --- |
| Invocations per environment | one at a time | several at the same time |
| Isolation | Firecracker microVMs on a shared fleet | containers on EC2 Nitro instances in your account |
| Billing | per request and duration | EC2 price plus a 15% management fee |
| Scaling | on demand, down to zero | asynchronously based on CPU utilization, not to zero |
| Good fit for | irregular traffic | high, predictable traffic |
For Node.js, LMI uses worker threads, and initialization runs once per worker thread. That matters for our example: if you load a large model into memory, expect it to sit in memory once per thread. In addition, concurrent requests in the same thread share the global state. Anything you create outside the handler should therefore only be read and never modified.
If you want to refresh the regular Lambda basics first, my article Deploy a Lambda function with CDK and TypeScript covers the standard path with a ZIP package.
Why large images slow down Managed Instances
Without SOCI, Lambda has to download and unpack the complete image on every instance before an execution environment can start there. That does not happen just once when you publish. It also happens whenever the capacity provider launches new instances because load is rising. With a 5 GB image, you are mostly waiting on network and disk during this phase.
The catch: a large part of that data is often never read. An image with PyTorch, several models and half an operating system contains far more than a single invocation needs. This is where SOCI lazy loading helps.
How SOCI lazy loading works
SOCI stands for Seekable OCI. OCI is the open standard for container images. The project comes from AWS and is open source under Apache-2.0. SOCI does not change your image. Instead, it creates an additional index that records for each file in which layer and at which position it is stored. With this table of contents, Lambda can fetch individual files from ECR instead of downloading everything in one go.
On Lambda Managed Instances, the flow looks like this: you push the image and the SOCI index together to ECR under a separate tag. That tag points to an image index with two entries, the regular image manifest and the SOCI index. If your function points to this tag, Lambda detects the index when you publish the version and turns on lazy loading. There are no further settings.
> 🎬 Startup with and without SOCI – this animated diagram only runs on my site: watch it on sascha-ms.de
When the files are fetched
Files your init code reads are fetched by Lambda during initialization. Anything the handler touches only later gets pulled in during that invocation, which then takes correspondingly longer. According to the documentation, Lambda does not download the rest of the image in the background. That gives you the most important rule for your code: import every library and read every file you need in the init phase. Then your invocation durations stay consistent.
!Diagram: five steps from an arm64 image via nerdctl to a SOCI index and a Lambda function using tag v1-soci
From a plain image to an image index with a SOCI index: five steps to the function.
Prerequisites for SOCI lazy loading
- An AWS account in a Region where Lambda Managed Instances are available, and AWS CLI v2
- Node.js 22 or later, Docker with buildx and AWS CDK v2 (here aws-cdk-lib 2.275.0)
- For the SOCI step, a Linux machine with containerd, for example a small EC2 instance with Amazon Linux 2023 and a Graviton processor
- A function packaged as a container image for one architecture, here arm64
- The jq command line tool and basic knowledge of TypeScript and CDK
A quick glossary: ECR is the AWS container registry. nerdctl is a Docker-like command line tool for containerd, and the SOCI snapshotter is the containerd extension that can create and read SOCI indexes.
Setting up SOCI lazy loading step by step
Our example is a recommendation service for a library. For every book ID, it returns the five most similar books. It computes the similarity from vectors stored in one binary file per language. The image contains models for German, English, French and Spanish, but the function reads only one of them. I chose this on purpose, because exactly this pattern shows up in real images all the time.
Step 1: Create the project
mkdir lmi-soci-library && cd lmi-soci-library
mkdir -p src scripts lib bin test
npm init -y
Replace the generated package.json with this version. "type": "module" makes Node.js load the compiled files as ES modules, including inside the container.
{
"name": "lmi-soci-library",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"models": "node --experimental-strip-types scripts/make-models.ts",
"test": "node --experimental-strip-types --test test/*.test.ts",
"measure": "node --experimental-strip-types scripts/measure-start.ts"
},
"dependencies": {},
"devDependencies": {
"@aws-sdk/client-lambda": "3.1159.0",
"@types/node": "^22.19.0",
"aws-cdk": "2.1146.1",
"aws-cdk-lib": "2.275.0",
"constructs": "^10.4.2",
"typescript": "7.0.2"
}
}
You run the scripts under scripts/ and the tests directly from TypeScript, using the Node.js 22 option --experimental-strip-types. For the container, on the other hand, you compile to dist/. Two small configuration files handle this:
// tsconfig.json
{
"compilerOptions": {
"target": "es2023",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"skipLibCheck": true,
"types": ["node"],
"outDir": "dist"
},
"include": ["src", "scripts", "lib", "bin", "test"]
}
// tsconfig.build.json
{
"extends": "./tsconfig.json",
"compilerOptions": { "rootDir": "src", "outDir": "dist" },
"include": ["src"]
}
npm install
Step 2: Load and compare vectors
The module reads a file in the format “magic bytes, count, dimensions, values” and compares books using cosine similarity. Cosine similarity measures how closely two vectors point in the same direction: 1 means very similar, 0 means unrelated.
// src/vectors.ts
import { readFileSync } from 'node:fs';
export interface BookVectors {
count: number;
dims: number;
data: Float32Array;
}
const MAGIC = 'BKV1';
const HEADER_BYTES = 12;
/** Writes vectors in a simple binary format: "BKV1", count, dims, float32 values. */
export function encodeVectors(v: BookVectors): Buffer {
const header = Buffer.alloc(HEADER_BYTES);
header.write(MAGIC, 0, 'ascii');
header.writeUInt32LE(v.count, 4);
header.writeUInt32LE(v.dims, 8);
const body = Buffer.from(v.data.buffer, v.data.byteOffset, v.data.byteLength);
return Buffer.concat([header, body]);
}
export function decodeVectors(buf: Buffer): BookVectors {
if (buf.subarray(0, 4).toString('ascii') !== MAGIC) {
throw new Error('Unknown file format: magic bytes missing');
}
const count = buf.readUInt32LE(4);
const dims = buf.readUInt32LE(8);
const expected = HEADER_BYTES + count * dims * 4;
if (buf.byteLength !== expected) {
throw new Error(`File size ${buf.byteLength} does not match ${expected} bytes`);
}
// Copy into an aligned buffer so Float32Array works on every offset
const data = new Float32Array(count * dims);
data.set(new Float32Array(buf.buffer.slice(buf.byteOffset + HEADER_BYTES, buf.byteOffset + expected)));
return { count, dims, data };
}
export function loadVectors(path: string): BookVectors {
return decodeVectors(readFileSync(path));
}
function cosine(v: BookVectors, a: number, b: number): number {
let dot = 0;
let na = 0;
let nb = 0;
const oa = a * v.dims;
const ob = b * v.dims;
for (let i = 0; i < v.dims; i++) {
const x = v.data[oa + i];
const y = v.data[ob + i];
dot += x * y;
na += x * x;
nb += y * y;
}
return na === 0 || nb === 0 ? 0 : dot / Math.sqrt(na * nb);
}
export interface Recommendation {
bookId: number;
score: number;
}
/** Returns the books most similar to bookId (without bookId itself). */
export function similarBooks(v: BookVectors, bookId: number, limit = 5): Recommendation[] {
if (!Number.isInteger(bookId) || bookId < 0 || bookId >= v.count) {
throw new RangeError(`bookId must be between 0 and ${v.count - 1}`);
}
const best: Recommendation[] = [];
for (let other = 0; other < v.count; other++) {
if (other === bookId) continue;
const score = cosine(v, bookId, other);
if (best.length < limit || score > best[best.length - 1].score) {
best.push({ bookId: other, score });
best.sort((x, y) => y.score - x.score);
if (best.length > limit) best.pop();
}
}
return best.map((r) => ({ bookId: r.bookId, score: Math.round(r.score * 1000) / 1000 }));
}
Step 3: A handler that loads in the init phase
The handler loads the model at the top level, which means during initialization. With SOCI, this is the moment Lambda fetches the file from ECR. If the call to loadVectors lived inside the handler, the first invocation of every environment would take noticeably longer.
// src/handler.ts
import { loadVectors, similarBooks, type Recommendation } from './vectors.ts';
// Load in the init phase: with SOCI, Lambda fetches the file here
// and not during the first invocation.
const MODEL_FILE = process.env.MODEL_FILE ?? '/opt/models/books-de.bin';
const started = Date.now();
const vectors = loadVectors(MODEL_FILE);
console.log(JSON.stringify({ msg: 'model loaded', file: MODEL_FILE, books: vectors.count, ms: Date.now() - started }));
export interface RecommendEvent {
bookId: number;
limit?: number;
}
export interface RecommendResult {
model: string;
recommendations: Recommendation[];
}
// Read-only shared state: safe even when an execution environment
// handles several invocations at the same time.
export async function handler(event: RecommendEvent): Promise<RecommendResult> {
const limit = Math.min(Math.max(event.limit ?? 5, 1), 20);
return { model: MODEL_FILE, recommendations: similarBooks(vectors, event.bookId, limit) };
}
Step 4: Generate test data and check locally
The script creates four model files with random values. With BOOKS=200000, every file grows to roughly 100 MB, and the difference at startup becomes clearly visible. Small files are enough for the first test.
// scripts/make-models.ts
// Generates one vector file per language. Only one of them is used per function –
// exactly the situation in which SOCI lazy loading pays off.
import { mkdirSync, writeFileSync } from 'node:fs';
import { encodeVectors } from '../src/vectors.ts';
const count = Number(process.env.BOOKS ?? 20000);
const dims = Number(process.env.DIMS ?? 128);
const languages = (process.env.LANGS ?? 'de,en,fr,es').split(',');
mkdirSync('models', { recursive: true });
let seed = 42;
const random = () => {
seed = (seed * 1664525 + 1013904223) % 2 ** 32;
return seed / 2 ** 32 - 0.5;
};
for (const lang of languages) {
const data = new Float32Array(count * dims);
for (let i = 0; i < data.length; i++) data[i] = random();
const file = `models/books-${lang}.bin`;
const buf = encodeVectors({ count, dims, data });
writeFileSync(file, buf);
console.log(`${file}: ${(buf.byteLength / 1024 / 1024).toFixed(1)} MB`);
}
BOOKS=5000 npm run models
npm test
npm run build
MODEL_FILE=models/books-de.bin node -e "import('./dist/handler.js').then(async (m) => console.log(await m.handler({ bookId: 7, limit: 3 })))"
Expected result: four files of 2.4 MB each, four passing tests and an output with three recommendations. Book IDs and scores are random; the structure looks like this:
{"msg":"model loaded","file":"models/books-de.bin","books":5000,"ms":4}
{
model: 'models/books-de.bin',
recommendations: [
{ bookId: 37, score: 0.337 },
{ bookId: 1965, score: 0.311 },
{ bookId: 3426, score: 0.274 }
]
}
Step 5: Build the image for arm64 and push it to ECR
The Dockerfile uses the official Lambda base image for Node.js 24. The models end up in /opt/models.
# Dockerfile
FROM public.ecr.aws/lambda/nodejs:24
# Compiled code plus package.json (for "type": "module")
COPY package.json ${LAMBDA_TASK_ROOT}/
COPY dist/ ${LAMBDA_TASK_ROOT}/
# All language models – each function only reads one of them
COPY models/ /opt/models/
CMD ["handler.handler"]
Two build options matter here. --platform linux/arm64, because Lambda accepts only single-architecture images. And --provenance=false, because buildx would otherwise attach an attestation to the image and turn it into an image index with several entries. For the SOCI step, you want a plain image without such extras.
BOOKS=200000 npm run models && npm run build
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=eu-central-1
REPO=library-recommender
REGISTRY=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
aws ecr create-repository --repository-name $REPO --region $REGION
aws ecr get-login-password --region $REGION | docker login -u AWS --password-stdin $REGISTRY
docker buildx build --platform linux/arm64 --provenance=false --sbom=false \
-t $REGISTRY/$REPO:v1 --push .
Step 6: Create the SOCI index
You run this step on a Linux machine with containerd. The easiest option is a small Graviton instance with Amazon Linux 2023, because its architecture already matches the image. The instance needs a role with read and write access to the ECR repository.
sudo yum install -y soci-snapshotter containerd nerdctl
sudo systemctl start soci-snapshotter
sudo systemctl restart containerd
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=eu-central-1
REPO=library-recommender
REGISTRY=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
aws ecr get-login-password --region $REGION | sudo nerdctl login -u AWS --password-stdin $REGISTRY
sudo nerdctl pull --platform linux/arm64 $REGISTRY/$REPO:v1
sudo nerdctl image convert --soci --platform linux/arm64 $REGISTRY/$REPO:v1 $REGISTRY/$REPO:v1-soci
sudo nerdctl push --platform linux/arm64 $REGISTRY/$REPO:v1-soci
nerdctl image convert --soci produces an index in format v2, and that is the only format Lambda accepts. When you follow older guides on the web, check which format they produce: Lambda does not support indexes in format v1.
Step 7: Check the result in ECR
Before you touch the function, take a look at the new tag. The manifest behind v1-soci must have exactly two entries, and one of them must be the SOCI index.
aws ecr batch-get-image --repository-name $REPO --region $REGION \
--image-ids imageTag=v1-soci \
--accepted-media-types application/vnd.oci.image.index.v1+json \
--query 'images[0].imageManifest' --output text | jq '.manifests | length, [.[] | .artifactType // .mediaType]'
Expected result: the first line shows 2, and the list contains an entry with soci in its name. If you see a higher number, attestations or additional platforms are still attached to the image. In that case, rebuild the image as in step 5.
Step 8: Capacity provider and function with CDK
Since version 2.235.0, aws-cdk-lib has a dedicated CapacityProvider construct. If you do not pass your own, it also creates the operator role with the managed policy AWSLambdaManagedEC2ResourceOperator. You attach a function to the provider with addFunction. The function is then published automatically as $LATEST.PUBLISHED, so you do not have to create a version by hand.
// lib/library-stack.ts
import { CfnOutput, Duration, Stack, type StackProps } from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import type { Construct } from 'constructs';
export interface LibraryStackProps extends StackProps {
repositoryName: string;
/** Tag of the SOCI-indexed image, e.g. "v1-soci" */
imageTag: string;
/** Optional: original tag (e.g. "v1") for a comparison function without SOCI */
plainImageTag?: string;
}
export class LibraryStack extends Stack {
constructor(scope: Construct, id: string, props: LibraryStackProps) {
super(scope, id, props);
// Private subnets with a NAT gateway so the instances can reach ECR and CloudWatch
const vpc = new ec2.Vpc(this, 'LibraryVpc', { maxAzs: 3, natGateways: 1 });
const sg = new ec2.SecurityGroup(this, 'InstancesSg', { vpc, allowAllOutbound: true });
const capacity = new lambda.CapacityProvider(this, 'LibraryCapacity', {
subnets: vpc.privateSubnets,
securityGroups: [sg],
architectures: [lambda.Architecture.ARM_64],
maxVCpuCount: 12,
});
const repo = ecr.Repository.fromRepositoryName(this, 'Repo', props.repositoryName);
const addRecommender = (id: string, tag: string) => {
const fn = new lambda.DockerImageFunction(this, id, {
code: lambda.DockerImageCode.fromEcr(repo, { tagOrDigest: tag }),
architecture: lambda.Architecture.ARM_64,
memorySize: 4096,
timeout: Duration.seconds(30),
environment: { MODEL_FILE: '/opt/models/books-de.bin' },
});
capacity.addFunction(fn, {
perExecutionEnvironmentMaxConcurrency: 8,
latestPublishedScalingConfig: { minExecutionEnvironments: 3, maxExecutionEnvironments: 6 },
});
new CfnOutput(this, `${id}Name`, { value: fn.functionName });
return fn;
};
addRecommender('Recommender', props.imageTag);
if (props.plainImageTag) {
addRecommender('RecommenderPlain', props.plainImageTag);
}
}
}
// bin/app.ts
import { App } from 'aws-cdk-lib';
import { LibraryStack } from '../lib/library-stack.ts';
const app = new App();
new LibraryStack(app, 'LibraryRecommender', {
repositoryName: app.node.tryGetContext('repo') ?? 'library-recommender',
imageTag: app.node.tryGetContext('tag') ?? 'v1-soci',
plainImageTag: app.node.tryGetContext('plainTag'),
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});
// cdk.json
{
"app": "node --experimental-strip-types bin/app.ts"
}
A few values in the stack are deliberate: according to CDK, maxVCpuCount must be between 12 and 15,000. perExecutionEnvironmentMaxConcurrency: 8 allows eight concurrent invocations per environment, which is rather generous for our compute-heavy handler. For pure CPU work, more concurrency per vCPU does not help much; more memory, and with it more vCPUs per function, helps more.
npx cdk bootstrap
npx cdk deploy -c repo=library-recommender -c tag=v1-soci -c plainTag=v1
With -c plainTag=v1, the stack creates a second function that points to the image without a SOCI index. You need it for the comparison in the next step. The deployment takes a few minutes, because Lambda launches the instances.
Step 9: Measure startup time with and without SOCI
The script publishes a new version for each function and measures the time until it is Active. It is a rough but honest measurement: it includes pulling the image and initializing the environments.
// scripts/measure-start.ts
// Publishes a new version per function and measures the time until it is "Active".
// Usage: npm run measure -- recommender-plain recommender-soci
import {
GetFunctionConfigurationCommand,
LambdaClient,
PublishVersionCommand,
} from '@aws-sdk/client-lambda';
import { setTimeout as sleep } from 'node:timers/promises';
const client = new LambdaClient({});
async function timeToActive(functionName: string): Promise<number> {
const start = Date.now();
const { Version } = await client.send(new PublishVersionCommand({ FunctionName: functionName }));
for (;;) {
const cfg = await client.send(
new GetFunctionConfigurationCommand({ FunctionName: functionName, Qualifier: Version }),
);
if (cfg.State === 'Active') return (Date.now() - start) / 1000;
if (cfg.State === 'Failed') throw new Error(`${functionName}:${Version} failed: ${cfg.StateReason}`);
await sleep(5000);
}
}
const names = process.argv.slice(2);
if (names.length === 0) {
console.error('Please pass at least one function name');
process.exit(1);
}
for (const name of names) {
const seconds = await timeToActive(name);
console.log(`${name}: active after ${seconds.toFixed(0)} s`);
}
npm run measure -- <name of the plain function> <name of the SOCI function>
You will find the function names in the outputs of cdk deploy. Measure several times and compare the averages, because network and placement vary. It is also worth checking CloudWatch Logs for the model loaded line and the duration of each first invocation. If you measure numbers with your own image, I would love to read about them in the comments.
Common mistakes with SOCI lazy loading
- The function points to the old tag. The image URI must point to v1-soci, not to v1. Otherwise everything works, just without lazy loading.
- Index in the wrong format. Lambda supports only SOCI Index Manifest v2. Check for the media type application/vnd.amazon.soci.index.v2+json.
- Too many entries in the image index. Attestations, SBOMs or a multi-arch image lead to more than two entries in the index.
- Files read only in the handler. Then the first invocation of every environment pays the loading time, and your latency metrics get outliers.
- Mismatched architecture. Capacity provider, function and image must share the same architecture, here arm64 everywhere.
- New image, old index. Every change to the image needs a new SOCI tag, for example v2-soci, and an updated image URI.
Costs and security
The announcement does not mention a separate price for SOCI itself. The index is stored as an additional artifact in ECR and is small compared to the image. The real costs come from Lambda Managed Instances: you pay the EC2 price of the instances plus a 15 percent management fee. Savings Plans and Reserved Instances apply only to the EC2 part. In this example, you also pay for a NAT gateway and the EC2 instance used for the SOCI step. Clean up after testing:
npx cdk destroy
aws ecr delete-repository --repository-name library-recommender --region eu-central-1 --force
On the security side, the capacity provider is the security boundary. Functions that share a provider run as containers on the same instances. Separate functions with different protection needs by giving them their own capacity providers. The operator role needs only the managed policy, and the role of the build instance needs access to just the one repository.
My take
SOCI lazy loading solves a real problem, but a narrow one. It helps only container image functions on Lambda Managed Instances, not the classic Lambda most of us use every day. If you run large images there, an extra build step gets you faster starts when publishing and when scaling out. That is mainly interesting for ML workloads, where an image ships many models or libraries.
Where I am cautious: the build process gets more complicated. You need containerd and nerdctl, a Linux machine with the matching architecture and a second tagging scheme. That belongs in the CI pipeline, not on someone’s laptop. And lazy loading is no substitute for lean images. If your function ends up reading almost everything anyway, you gain little. My advice: clean up the image first, then measure, then add SOCI. By the way, for Java functions on regular Lambda, SnapStart remains the tool of choice for startup times.
FAQ
Does SOCI lazy loading work with regular Lambda?
No. As of late October 2026, Lambda supports SOCI only for functions that are packaged as container images and run on Lambda Managed Instances.
Do I have to change my function code for SOCI?
Not necessarily. You should, however, load all libraries and files in the init phase, so the first invocation of an environment is not slowed down by on-demand fetching.
Can I create the SOCI index with Docker?
The AWS documentation shows the path with containerd, nerdctl and the SOCI snapshotter. Docker alone does not create a SOCI index, so you need an environment with containerd for this step.
How much faster does my function start with SOCI?
AWS quotes startup times that are up to 70 percent shorter. How much you gain depends on which share of the image is actually read. Measure it with and without the index.
Your next step
Take your largest container image and find out which share of it is really read at startup. If it is less than half, and you are considering Lambda Managed Instances anyway, SOCI is a good candidate for an experiment. What makes your Lambda images so large: models, libraries or something else entirely?
Sources
- AWS What’s New: AWS Lambda supports SOCI lazy loading for Lambda Managed Instances (Oct 28, 2026)
- AWS docs: Lazy-load container images with Seekable OCI (SOCI)
- AWS docs: Lambda Managed Instances
- AWS docs: Getting started with Lambda Managed Instances
- AWS docs: Node.js runtime for Lambda Managed Instances
- AWS docs: Lambda Managed Instances runtimes
- AWS Compute Blog: Build high-performance apps with AWS Lambda Managed Instances
- GitHub: awslabs/soci-snapshotter
- AWS CDK API Reference: aws_lambda.CapacityProvider
This post was created with AI assistance.
---
👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser.
Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de
1 reaction
AI-DLC Workflows v2 よい。はじめ「?」っとなったが、IntentsとCustom Scope使ったハンドリングに慣れてくると心地よくなってきた
github.com/awslabs/aidlc-workflows/tree/main
3 likes · 40 views
Hijacking a Refund Agent Through Poisoned Support Tickets: Cristian Leo and Daniel Begimher at Global AppSec USA 2026
Players in this hands-on pod attack a customer-returns AI agent that can read tickets, fetch URLs, look up orders, send email or issue refunds.
A player wins by making the agent issue a refund, email an address the player picks or post order data to a URL the player controls.
No payload is handed out. Players write the support ticket, the tracking page or the email that the agent is asked to summarize, then hide their instructions inside it.
In their Global AppSec USA 2026 hands-on pod, "Hijack the Agent", on Thursday, November 5 at 10:10 a.m. in Marina, Cristian Leo and Daniel Begimher let attendees attack an AI agent that defends itself a little better at each stage.
Speakers:
- Cristian Leo, Co-Founder and CTO, Armorer Labs
- Daniel Begimher, Co-Founder and CEO, Armorer Labs
The pod runs the same app through four stages, each one harder. At the beginner stage the tools are wide open, so even direct injection in the chat works.
At the intermediate stage a chat filter is on, so players must deliver their instructions through the web page or the email instead. That maps to ASI01, Agent Goal Hijack, in the OWASP Top 10 for Agentic Applications.
At the advanced stage a static deny-list guards the issue_refund and send_email tools, yet the same string passes through lookup_order. Players must find a tool call the list did not anticipate, then a new fetch_url tool makes the previous round's rule wrong. That maps to ASI02, Tool Misuse.
The final "Impossible" stage adds an in-process dispatcher hook that inspects every tool call. Attacks from the first three stages die there with a stated reason, which is the lesson.
Each stage takes under 15 minutes, and players need only a laptop with a browser. The pod repeats on Thursday at 3:00 p.m., then on Friday at 11:30 a.m. and 1:30 p.m.
Cristian Leo is Co-Founder and CTO of Armorer Labs, a Y Combinator Fall 2026 company that tests AI agents for exploitable failures. Previously at Amazon Web Services, he is corresponding author of ThreatForest, a multi-agent system that turns a source repository into attack trees mapped to MITRE ATT&CK or CAPEC.
Daniel Begimher is Co-Founder and CEO of Armorer Labs. At AWS he led development of AWS's first customer-facing AI security agent, built red-team agents and wrote the open-source scanner Automated Security Helper.
Leo and Begimher co-presented ThreatForest at Black Hat USA 2026. They also co-authored SIR-Bench, a 794-case benchmark that measures whether incident response agents actually investigate or merely triage.
Begimher also co-wrote the AWS Security Blog post "Balancing speed and safety: A control framework for AI coding agents," which pairs scoped tool access for AI coding agents with build-time gates.
To go deeper on the topic, start here:
- OWASP Top 10 for Agentic Applications for 2026
- ThreatForest Black Hat USA 2026 slides
- ThreatForest paper (arXiv)
- SIR-Bench paper (arXiv)
- ThreatForest code (aws-samples)
AppSec engineers who need to see why deny-lists fail against agent tool calls will get a fast, concrete demonstration here.
Which of your AI agent's tools would an attacker reach for first through a poisoned document?
2 points · 1 comment
Mentions
aidlc-workflowsのbugfixスコープでバグ修正してみた — AIとのやり取りと判断の記録 はじめに こんにちは、カンリー店舗集客チームの三上です。 以前、AWSが公開しているaidlc-workflowsを使った機能開発の設計フェーズで、どのようにAIの成果物をレビューし、チームで意思決定しているかを紹介しました。 zenn.dev 今回は少し趣向を変えて、aidlc-workflowsのbugfixスコープを使って、実際にバグ修正をリリースするまでの過程を紹介したいと思います。 対象となったのは、Googleビジネスプロフィールに連携するデータのCSV一括更新で、メニューやサービスの説明文の文字数制限が、入力経路によって食い違っていた不具合です。 当初のチケットには修正方針も書かれており、そのとおりに実装すればよさそうに見えました。しかし、ワークフローに沿ってコード調査や要件分析を進めるうちに、その修正方針自体を見直すことになりました。 AIとのやり取りの詳細を振り返ってみると、AIの調査結果をそのまま採用することもあれば、根拠を問い直し、提示された選択肢を選ばずに調査をやり直すこともありました。 この記事では、そうしたAIとのやり取りや判断を、実際の作業の流れに沿って紹介します。 ぜひ「自分だったらここでどう判断するだろう?」と考えながら読んでみてください。 bugfixスコープのステージ構成 本題に入る前に、aidlc-workflowsの進め方について触れておきます。以下のステージ構成は、aidlc-workflows v2.5.10での内容です。 aidlc-workflowsでは、開発の工程が「ステージ」という単位に分かれています。ステージは全部で32個あり、5つのフェーズ(Initialization / Ideation / Inception / Construction / Operation)に属しています。 どのステージを実行するかは「スコープ」で決まります。bugfixスコープでは、32ステージのうち7ステージだけを実行し、残りはスキップします。 bugfixスコープは、既存コードの理解、修正要件の明確化、実装と検証に絞った構成です。新機能開発で実施する市場調査やアーキテクチャ設計などはスキップします。 フェーズ ステージ 役割 Initialization workspace-scaffold / workspace-detection / state-init 作業記録の置き場を用意し、対象のコードベースの言語やフレームワークを判定して、どのステージを実行するかの計画を作ります。自動で進みます Inception reverse-engineering 既存のコードを読み、構成・API・依存関係などをドキュメントにまとめます。以降のステージは、ここでまとめた内容を前提に進みます Inception requirements-analysis 依頼内容を分析し、曖昧な点や判断が必要な点を洗い出して、要件定義書にまとめます Construction code-generation まず実装計画を作って人の承認を取り、そのあとで計画に沿ってコードを書きます。最後に変更内容のまとめを残します Construction build-and-test ビルドとテストの手順書を作り、テストを実行して結果をまとめます 1つのステージの回し方 大まかな流れは以下の通りです。 判断が必要なステージ(今回はrequirements-analysisとcode-generation)は、AIが成果物を一気に作って人が最後に承認する、という流れではありません。AIが発行した設問を通して、人と認識をすり合わせながら成果物を作っていきます。 設問と回答はファイルとして残るので、あとから「なぜその判断になったのか」を辿ることもできます。 修正するバグについて Googleビジネスプロフィール(以下、GBP)では、店舗ごとにメニューの情報や提供するサービスの情報を設定することができます。 不具合の内容は、「GBPに連携するメニューとサービスの説明文について、CSV一括更新時の文字数制限が、ほかの入力経路と食い違っている」というものでした。 チケットには、メニューとサービスそれぞれについて、次の修正方針が記載されていました。 メニュー:CSV一括更新時の文字数制限を300文字から1000文字に変更する サービス:メニューと対称的な構造になっているため、同様に1000文字に変更する つまり、メニューとサービスの両方で、CSV一括更新時の上限を1000文字に引き上げるという方針です。修正内容がチケットに書かれていたので、最初はその方針に沿って実装を進めればよさそうに見えました。 ただ、今回はaidlc-workflowsのbugfixスコープを使うため、すぐにコードを変更するのではなく、まずreverse-engineeringで既存コードの調査から始めることになりました。 reverse-engineering - コード調査で修正方針に疑問が生じた 最初のreverse-engineeringステージでは、AIが既存のコードを調査し、システムの構成や関連する処理を整理していきます。 調査を始めた段階で、AIからチケットに書かれた修正方針について指摘がありました。 🤖 AI メニュー側(1000)とサービス側(300)を同じ値に揃えてよいのかという論点で、チケットの期待値そのものを変える可能性があります。 チケットでは、メニューとサービスが対称的な構造になっていることを理由に、両方の上限を1000文字へ変更する方針になっていました。 しかし、コードを確認すると、画面の入力欄ではメニューが1000文字、サービスが300文字と、もともと文字数上限が違っていました。さらに、GBPではメニューとサービスは別のAPIで扱われており、媒体側の仕様から見ても、上限が同じとは限りません。 同じ下調べでは、チケットに書かれていない修正候補も見つかっていました。サービスの説明文を受け付けるAPIのバリデーションで、上限が1023文字になっている箇所です。 この時点では、どちらの仕様が正しいのかまでは分かっていません。画面側の実装が正しいとも限りませんし、チケットの修正方針が間違っているとも断定できません。 修正内容が決まっているように見えたチケットでしたが、実装に入る前に、その前提を確認する必要が出てきました。 requirements-analysis - 前提を疑い、調査をやり直した requirements-analysisでは、reverse-engineeringで出てきた疑問を、一次資料にあたって確かめていきます。AIはGBPの公式ドキュメントを引き、確認した事実と、人に判断してほしい設問を整理していきました。 最初にまとまった推奨案は、サービスの文字数上限を1000文字にせず、300文字のまま据え置くというものでした。 ただ、ここからの数時間で、AIが示した根拠を私が問い直す場面が何度もありました。 AIが参照したAPIは正しいのか 最初に気になったのは、AIがサービスの仕様の出典として挙げていたAPIです。 店舗集客がサービスの更新に使っているのは、Business Information APIのlocations.patchですが、AIは「ServiceList API」を参照していました。 このプロダクトに関わって3年ほど経ちますが、そんな名前のAPIを扱っている実装は見たことがありませんでした。 🤔 三上 ServiceList APIって、どのAPIを指してます? 店舗集客で使っているAPIの情報を添えて聞き返すと、非推奨となった旧APIを出典にしていたことが分かりました。現行APIを確認し直しても値は変わりませんでしたが、参照先は修正してもらいました。 この調査の中で、APIドキュメントの記載が英語版と日本語版で異なっていることもわかりました。英語版にはサービスの説明について「250文字以下を推奨」とありますが、日本語版には「半角250文字(全角125文字)以下」と書かれています。 この違いを確認したうえで、AIから本番データとエラーログを調査する提案がありました。 「GBPは250文字を強制していない」は本当か AIは過去30日分のエラーログを調査し、文字数超過による更新エラーが0件だったと報告しました。また、DBを確認し、全角125文字を超えるデータが実際に既に存在していることもわかりました。 調査結果を踏まえて、AIは次のように結論づけました。 🤖 AI GBP の 250 は推奨であって強制されておらず、全角 125 文字超(半角に換算すると250文字を超えるデータ)がありながら 30 日間の拒否は 0 件でした。 AIはこの結論を根拠に、サービスの上限を300文字のまま据え置く方針で確定させようとしていました。 しかし、それが根拠として十分なのかが気になりました。その理由は、ログ調査では対象期間を30日に絞っていたのに対して、DBで実行したクエリは作成時期を問わず、今ある全データのうち対象が何件あるかを数え上げるものだったからです。 🤔 三上 過去30日で弾かれたのは0件だったが、たまたま直近30日で発生していなかっただけの可能性はないですか?DBの取得結果は全期間を対象としています。ログ調査の期間内に作成されたデータがあるなら納得できますが、DBの結果と照らし合わせた結論でしょうか? 30日間エラーがなかったことと、GBPが文字数上限を強制していないことは別の話です。AIも推論が飛躍していたことを認め、結論を撤回しました。 🤖 AI DBの取得結果には作成日を含んでおらず、確認不足でした。「強制されていない」は取り下げ、未確認に戻すべきです。 AIは誤った記述の修正計画とあわせて、未確認の点を埋めるための追加調査の案を3つ示しました。 A. 未確認のまま追加調査をしない B. ログの検索期間を90日に広げてもう一度確かめる C. GBPから取り込んだデータとDBの値を付き合わせる 提示された3択を選ばず、前提から調べ直す AIが示した追加調査案を受け入れず、このように返しました。 🤔 三上 調査自体やり直したいです。前提となる事実全体を調べ直したいです。 この時点までに、旧APIを出典にしていたこと、観測したデータから結論を急いでいたことなど、根拠や推論の誤りが続けて見つかっていたからです。 前提や根拠を誤ったまま判断してしまうと、「GBPのメニュー・サービスの情報を適切に更新できるようにする」というバグ修正の本来の目的を果たせなくなる可能性があります。修正方針を立てる上で、納得できる根拠を持っておくべきと考えました。 GBPの公式ドキュメント、実際のAPI呼び出し経路、DB、エラーログ、チケットに記載された値などを改めて確認しました。エラーログも、過去30日ではなく1年分を調べました。 再調査によって、次のことが確認できました。 公式ドキュメントの文字数には、上限ではなく推奨値として記載されているものがあった 過去1年分のログでも、文字数超過による更新エラーは確認できなかった 店舗集客からGBPへ送信する経路では、GBPへの更新が成功しなければDBに保存されないことが確認できた GBPから取り込んだデータに、全角で250文字を超えるサービスの説明文が存在した 推奨値を超える文字数の説明文が実際に存在することは確認できましたが、APIが受け付ける上限はまだ分かっていませんでした。そこで、GBPの管理画面に手がかりを求めることにしました。 GBPの管理画面でも上限を確認する GBPの管理画面を確認すると、サービスの説明文は300文字、メニューの説明文は1000文字までしか入力できない作りになっていました(いずれも全角半角を問わない)。店舗集客の画面側で設定されていた上限と一致しています。 さらに、GBP APIを使った更新も試し、画面と同様にサービスの説明文は300文字、メニューの説明文は1000文字まで受け付けられることを確かめました。 ここまでの結果を踏まえ、サービスの説明文は300文字のまま変更しない方針にしました。最初にAIが示した方針と同じ結論になりましたが、再調査を入れたことで、その判断を支える十分な根拠を得ることができました。 一方、サービス名は管理画面で120文字までしか入力できませんでしたが、公式ドキュメントの推奨値は140文字でした。 AIは当初、サービス名を「今回の修正の対象外」としていました。しかし、ユーザーが入力できる文字数に関わるため、PdMに方針を確認したいと考えました。 翌日、PdMとも修正方針を合意した上で、当初修正対象にしていなかったサービス名も、GBPの管理画面上の仕様に合わせて120文字の制限をつけることに決めました。 レビュー用AIの指摘を、実際のデータで判断する ここまでの調査結果を踏まえて、無事AIが発行した設問にすべて回答できました。その回答をもとに要件定義書が作成されると、レビュー用のAIが内容を確認します。 AIのレビューの中で、OpenAPIに文字数上限を追加することについて、次の懸念が指摘されました。 OpenAPIに文字数上限を追加すると、画面側の入力チェックにも自動で反映される すでに文字数上限を超えたデータを持つ店舗では、編集画面で理由が表示されないまま保存ボタンが押せなくなる可能性がある つまり、OpenAPIを変更することで、「文字数の上限を超えている」という事実がわからないまま、ただ保存ボタンが押せない状態となって困るユーザーがいるのではないかという懸念です。 まずはこの懸念に該当するデータが実際に存在するのか確認した上で判断したいと思い、DBを確認しました。幸い、懸念のケースに該当するデータはありませんでした。 既存データへの影響が出ないことが確認できたので、AIが指摘した懸念を踏まえたうえで、OpenAPIにも上限を追加する方針にしました。 このステージで決まったこと 要件は、チケットに書かれていた修正方針から次のように変わりました。 メニュー:CSV一括更新の上限を300文字から1000文字に引き上げる(チケットどおり) サービス:CSV一括更新の上限は300文字のまま据え置く(チケットでは1000文字に変更する予定だった) チケットに書かれていなかったもの:APIのサービス説明文の上限を1023文字から300文字に変更し、サービス名の上限を120文字に揃える。OpenAPIに文字数上限を追加し、仕様書の記述も修正する このように、最終的にはチケットに書かれた修正方針を一部見直し、当初の対象に含まれていなかった箇所も修正することになりました。 code-generation / build-and-test - 計画を立ててから実装し、テストする 店舗集客では、GBPと連携するデータの更新が、次のように複数のシステムにまたがっています。 今回の修正も、それぞれに手を入れる必要がありました。requirements-analysisで決めた修正方針と修正対象の対応は以下の通りです。 システム 役割 修正内容 OpenAPI定義 APIの仕様の正本。変更すると、管理画面とAPIのコードに自動で反映される メニュー・サービスの説明文とサービス名に、文字数の上限を追加 管理画面 店舗の情報を編集する画面 OpenAPI定義の変更が自動で反映されるのを確認 店舗集客のAPI 管理画面からの更新を受け付け、GBPへ送る サービスの説明文の上限を1023文字から300文字に、サービス名の上限を120文字に変更 worker CSV一括更新を処理する メニューの説明文の上限を300文字から1000文字に変更 仕様書 機能の仕様をまとめたドキュメント 上限値の記述を修正 code-generationでは、AIがいきなりコードを書くのではなく、まず実装計画を作ります。今回の計画では、複数のシステムにまたがる修正を5本のPRに分け、どの順番でマージするかまで整理していました。 計画を承認すると、AIが計画に沿ってコードを書き、最後にレビュー用のAIが変更内容を確認します。 分からない懸念は、分かるまで聞く GBPのサービスには、Googleがあらかじめ用意している定型のサービス(StructuredServiceItem)と、店舗が自由に登録するサービス(FreeFormServiceItem)の2種類があります。 店舗集客の画面から編集できるのは、FreeFormServiceItemだけです。要件分析で本番データの文字数を確かめたのも、こちらのサービスでした。 実装の途中で、コードを書いていたAIから懸念が1つ上がってきました。 店舗集客のAPIでサービス名の上限を120文字にすると、文字数を確かめていないStructuredServiceItemの名前にも上限がかかってしまう、という内容です。 しかし、StructuredServiceItemは店舗集客では扱っておらず、GBPへのデータ連携の対象外とした実装になっています。対象外のデータについて何を心配しているのか、私にはすぐには分かりませんでした。 🤔 三上 ちょっと言ってる意味がわからなかったです。懸念事項について、もう少し詳しく説明してもらえますか? 聞き返すと、AIはあらためてコードを確認し、懸念を取り下げました。 🤖 AI サブエージェントの読み違いで、実害の懸念は成立しません。撤回します。 懸念の出どころは、実装を担当したAIが、仕様書の1文から推測したものでした。AIの調べ直しで、バリデーションはFreeFormServiceItemのみに適用されており、StructuredServiceItemへの影響がないことがわかりました。 分からないまま「OK、それで」と進めていたら、不要な対応を追加していたかもしれません。 ドキュメントの書き方は、マージの順番で解決する 実装のあと、レビュー用のAIから次の指摘がありました。 仕様書に、まだリリースしていない新しい上限値が、現在の値として書かれている AIの修正案は、仕様書に「リリース前の値はこうだった」という注記を書き足すものでした。 ただ、そうするとリリースしたあとに、注記を消すためにもう一度仕様書を直すことになります。 🤔 三上 リリース前である旨を記載するとリリース後に再度修正が発生するので手間です。マージ順序でカバーできないですか 最終的に、仕様書のPRは、実装のPRがすべてリリースされたあとにマージする順番にしました。 仕様書を書き直すのではなく、マージの順番で整合を取った形です。 最後にbuild-and-testで、ビルドとテストの手順書を作り、テストを実行しました。 バグ修正の意図通りに実装されているか確かめるため、上限ちょうどの文字数と1文字超えた文字数を確かめる単体テストを追加し、ローカル環境とCIで確認しました。 ワークフローはここまでで、検証環境での動作確認とリリースは、ワークフローの外で進めました。会議や他のタスクを複数並行している中でも、着手から1スプリント(1週間)でリリースすることができました。 おわりに 今回は、aidlc-workflowsのbugfixスコープを使って、バグ修正をリリースするまでの過程を紹介しました。 チケットに書かれた修正方針を見直し、当初は対象に含まれていなかった箇所も修正してリリースできました。 修正方針の見直しにつながる指摘の多くは、AIの調査から出てきたものでした。一方で私がしていたのは、その根拠が判断に足りるかを確かめることと、提示された選択肢の外に進め方がないかを考えることでした。 ワークフローは、立ち止まる場面を設問やレビューの形で用意してくれましたが、そこで何を疑い、どこまで確かめるかは、自分で判断する必要がありました。 よく考えてみると、私がバグ修正の過程でAIに出した多くの指摘は、メンバーから上がってくる報告や相談を受ける時や、レビューなどで確認する観点とほとんど同じでした。AIとのやり取りも、普段の開発で行っている確認や議論の延長線上にあるのだと感じました。 AIと一緒に開発を進めていく上での一つの実践例としてご参考になれば幸いです!
Solana P-256 Syscalls: Agave and Firedancer, Edge Case by Edge Case Originally published on xroot.dev. On 27 October both of Solana’s independent runtimes merged the same new feature, written separately. Agave added P-256 curve operations to its syscalls at 02:38 UTC; Firedancer added its own sixteen hours later. Different authors, different languages, different arithmetic, and consensus needs them to return the same bytes for every input a program can send, including every input the specification rejects. SIMD-0565 extends Solana’s curve syscalls to P-256, also called secp256r1: the curve behind passkeys, WebAuthn and the secure chips in phones. We wrote our own reference implementation from the curve’s published constants, generated 626 test vectors aimed at the edges of the specification, ran them through the code Agave’s syscalls call, and checked Firedancer’s tests and source for the same cases. We found no case where the two differ. We also built a small program that verifies a P-256 signature with three of the new syscalls and no modular inverse. It costs 22,760 compute units. - * * What SIMD-0565 adds No new syscall names. The proposal adds two curve ids, 7 for little-endian and 0x87 for big-endian, to the four curve syscalls programs already use for curve25519 and BLS12-381: point validation, decompression, group operations (add, subtract, multiply by a scalar) and multiscalar multiplication. Points are 64 bytes, x then y, and the point at infinity is 64 zero bytes. Compressed points are 33 bytes: a 02 or 03 parity prefix, then x. Every call has a fixed price, charged before its inputs are read, whether it succeeds or not: Validate a point — 20 CU Decompress — 610 CU Add or subtract — 520 CU Multiply by a scalar — 9,000 CU Multiscalar, n points — 9,000 + 3,800 × (n − 1) Most points within 1.4M CU — 367 A multiscalar call with more than 512 points fails outright, before anything is charged. The SIMD’s authors set the prices on a private ten-validator cluster with 200 ms slots, so that a block packed with any one of these operations still replays within its slot. - * * Two implementations of one rulebook Agave’s version, #13918, opened in July, calls solana-secp256r1 0.1.0, a pure-Rust P-256 crate Anza published in September. Firedancer’s, #12498, opened on 22 October, is C built on AWS’s s2n-bignum routines where the build has them. What the two share is everything consensus can see: the same feature-gate key, the same seven compute prices, and five test vectors. Firedancer’s syscall tests reuse the expected outputs from Agave’s, in both byte orders. Agreement matters more than either implementation being right by its own tests. If one client returns 0 where the other returns 1, a program that branches on the result writes different account data on each, the two compute different bank hashes for the same block, and the network splits along client lines. The dangerous inputs are the ones nobody thought to try. - * * The edge cases, and what both clients do These are the inputs where a reasonable implementation could choose differently. In every row the two give the same answer: Agave on our vectors through the crate its syscalls call, Firedancer in its own tests or, where its tests don’t reach, in its source. 64 zero bytes — identity, valid x = 0, y = √b, a real point — valid (0, 1) — rejected x or y written as value + p — rejected Prefix other than 02 or 03 — rejected 33 zero bytes, SEC1’s identity — rejected P + P, or P − (−P) — 2P P − P, or k·P + (n − k)·P — identity 0·P, or k times the identity — identity Scalar n, n + 1 or 2²⁵⁶ − 1 — rejected Multiscalar, 0 points — identity Multiscalar, bad last scalar or point — rejected Multiscalar, over 512 points — InvalidLength Any P-256 call before activation — InvalidAttribute “Rejected” means the call returns 1 and writes nothing; the last two rows are errors that abort the transaction. Firedancer’s tests cover the identity, off-curve and out-of-range points, the 04 prefix, both byte orders, P + P, P − P, P + (−P), adding and subtracting the identity, 0·P, k times the identity, n·P, multiscalar calls of 0, 1, 2 and 70 points including a bad scalar or point, running out of compute and the gate being off. The x = 0 point, the value-plus-p encodings, the other prefixes, n + 1, doubling through subtraction and cancellation inside a multiscalar call we confirmed in its source. - * * 626 vectors, and three things to know before you call them Our reference is about 160 lines of Python written from the curve constants in NIST SP 800-186 and the SIMD text, without reading either client’s arithmetic. It reproduces all five shared vectors. From it we generated 313 vectors per byte order: 73 hand-picked cases and 240 random operations. The hand-picked ones include everything in the table above, 64-point multiscalar calls whose two 32-point halves are equal or cancel (Firedancer works in batches of 32 points, so these land on its batch boundary), and calls of 367 and 512 points. Run through solana-secp256r1 0.1.0, called exactly as Agave’s syscalls call it at the merge commit, all 626 match, including all 127 rejections. - x = 0 is on the curve. The identity’s encoding has x = 0, and so does a real point, (0, √b). Both clients treat only the exact all-zero encoding as the identity, so (0, √b) is an ordinary point, decompression of x = 0 succeeds, and (0, 1) is rejected. - Scalars are never reduced. A scalar of n, n + 1 or 2²⁵⁶ − 1 makes the call return 1, even when the point is the identity. A SHA-256 hash is a valid scalar only if it is below n, so reduce it first: any 256-bit value is below 2n, so one comparison and one subtraction are enough. - Little-endian is per coordinate. With curve id 7, x and y are each byte-reversed in place, and a compressed point keeps its prefix byte first with only x reversed, which is not SEC1. The generator written in the other byte order fails validation. - * * The one place the code differs Read side by side, the clients take different paths in one place: an unknown curve id or group operation in validation, group operations or multiscalar multiplication. Firedancer’s syscalls always abort with InvalidAttribute. Agave aborts only when the feature from SIMD-0137 is active, and otherwise returns 1. That feature has been active on mainnet since slot 311,904,000, the first slot of epoch 722, so the result is the same there. Nothing on the P-256 path itself branches this way. - * * A signature check from three syscalls The obvious use is checking passkey signatures inside a program, which today goes through the secp256r1 precompile. ECDSA verification normally needs s⁻¹ mod n, and no syscall computes it. It can be avoided. The signature is valid when s·R equals z·G + r·Q up to sign, where R is the point whose x-coordinate is r, z the message hash and Q the public key. Decompression gives R, one multiplication gives s·R, one two-point multiscalar call gives z·G + r·Q, and comparing x-coordinates ignores the sign: use solana_define_syscall::{ curve_constants::{GROUP_OP_MUL, SECP256R1_BE}, definitions::{sol_curve_decompress, sol_curve_group_op, sol_curve_multiscalar_mul}, }; /// ECDSA check: s·R == ±(z·G + r·Q), where R is the point with x = r. /// Same answer as the textbook check except for x(R) = r + n, which has /// probability about 2^-128 and is not tried here. pub fn verify_p256(q: &[u8; 64], z: &[u8; 32], r: &[u8; 32], s: &[u8; 32]) -> bool { if is_zero(r) || is_zero(s) || *r >= N || *s >= N { return false; } // The syscalls reject scalars >= n instead of reducing them. let z = if *z >= N { sub(z, &N) } else { *z }; let mut c = [0u8; 33]; c[0] = 0x02; // either parity: s·(−R) = −(s·R) has the same x c[1..].copy_from_slice(r); let (mut rp, mut u, mut t) = ([0u8; 64], [0u8; 64], [0u8; 64]); let scalars = [z, *r]; let points = [G, *q]; unsafe { // R: 610 CU if sol_curve_decompress(SECP256R1_BE, c.as_ptr(), rp.as_mut_ptr()) != 0 { return false; } // U = s·R: 9,000 CU if sol_curve_group_op(SECP256R1_BE, GROUP_OP_MUL, s.as_ptr(), rp.as_ptr(), u.as_mut_ptr()) != 0 { return false; } // T = z·G + r·Q: 9,000 + 3,800 CU; returns 1 if Q is not a valid point if sol_curve_multiscalar_mul( SECP256R1_BE, scalars.as_ptr() as *const u8, points.as_ptr() as *const u8, 2, t.as_mut_ptr(), ) != 0 { return false; } } t != [0u8; 64] && u[..32] == t[..32] } // N and G are the group order and generator, big-endian; is_zero and sub are byte helpers. We built it with platform-tools v1.57 and ran it in solana-sbpf 0.26.0, the VM version Agave pinned at the merge, set up as in our CPI measurement, with the three curve syscalls implemented the way Agave implements them on top of the same crate. OpenSSL signed 20 messages, and the program got each signature, the same signature with the other half of S, a hash with one bit flipped, and a different public key. All 80 verdicts match textbook ECDSA: Curve syscalls: 610 + 9,000 + 12,800 — 22,410 CU Memory syscalls — 30 CU Program instructions — 320 CU One signature check — 22,760 CU Precompile, cost-model charge — 4,800 CU Two differences from the precompile. Cost: a check through the syscalls takes about 4.7 times the 4,800 compute units the cost model charges per precompile signature, before the message is hashed. Policy: this check accepts both halves of S, where the precompile rejects the high one, the reason half of raw WebAuthn signatures fail it. A program that wants the precompile’s rule adds s ≤ n/2; one that doesn’t can skip normalising S altogether. The SIMD gives that freedom as the reason for curve operations instead of a single verify syscall, which would have had to fix one malleability rule for everyone. - * * Where it stands Both pull requests introduce the gate secp256r1_syscall_enabled, key SeRjtX7Abiq5hQYbzdCny266oveDseHZh9tnrs2cdVC, so the syscalls were off on every cluster when they merged, and a program calling them gets InvalidAttribute until the feature is activated. The SIMD itself was still marked Draft, with its compute prices added a week earlier. The program side is ready: solana-define-syscall 5.2.0, published in July, already declares the curve ids and all four syscalls, and our program imports them from there. - * * What this does not show We did not build or run Firedancer. Its side rests on reading its source and on its own CI run, which passed test_secp256r1 and test_vm_syscall_curve on the pull request’s final commit, whose P-256 files are identical to the merged ones. On Agave’s side we ran the crate its syscalls call, not the syscalls: memory translation, compute charging and the gate we read rather than executed. Vectors find disagreements; they cannot prove there are none. The compute prices are the SIMD’s calibration, not our measurement, and 22,760 is one program built with one toolchain. - * * P-256 Syscalls: Questions People Actually Ask What does SIMD-0565 add to Solana? It extends the four existing curve syscalls (point validation, decompression, group operations and multiscalar multiplication) to P-256, also called secp256r1, with curve id 7 for little-endian and 0x87 for big-endian encoding. Points are 64 bytes, x then y, with 64 zero bytes for the point at infinity. Agave and Firedancer both merged an implementation on 27 October 2026, behind a new feature gate. How much does it cost to verify a P-256 signature with the new syscalls? Our test program verified an ECDSA P-256 signature for 22,760 compute units: 22,410 for one decompression, one scalar multiplication and one two-point multiscalar multiplication, 30 for memory syscalls and about 320 program instructions. Hashing the message is extra. The secp256r1 precompile is charged 4,800 compute units per signature in the cost model. Do the secp256r1 syscalls reduce scalars modulo the group order? No. A scalar of n or more makes the call return 1 in both Agave and Firedancer, including n + 1 and 2^256 − 1. A program that uses a 256-bit hash as a scalar must reduce it first; since any 256-bit value is below 2n, one comparison and one subtraction are enough. How is the point at infinity encoded for Solana’s P-256 syscalls? As 64 zero bytes. Only that exact encoding is the identity: (0, √b), which also has x = 0, is an ordinary point on the curve, and (0, 1) is rejected. Decompression cannot produce the identity, and 33 zero bytes are rejected. Are the P-256 syscalls active on mainnet? Not when they were merged. Both pull requests introduced the feature gate secp256r1_syscall_enabled, so it was inactive on every cluster, and every P-256 call fails with InvalidAttribute until it is activated. - * * The one-line version Agave and Firedancer implemented SIMD-0565’s P-256 syscalls independently and agree on every edge case we could find, from the x = 0 point to unreduced scalars; with them, a program verifies a P-256 signature for 22,760 compute units and sets its own rule for S. Sources: SIMD-0565 at commit f13f5d65; Agave #13918, merged as cf58f070; Firedancer #12498, merged as 8e7388db; solana-secp256r1 0.1.0, solana-define-syscall 5.2.0 and solana-sbpf 0.26.0. The reference, vectors, harnesses and test program are our own.
AI 写代码越写越乱,没人把关、出了事也查不到? AWS 的 AI-DLC 管起开发全流程:5 阶段 33 步,7 个工具通用,关键节点要审批,全程留审计记录。 团队用 AI 写代码的可以看看,流程先立起来。 你的 AI 写代码有人审吗? #AI编程 #开发流程 @grok awslabs/aidlc-workflows
逆引き! Bedrock AgentCore gateway ユースケース38選 はじめに Amazon Bedrock AgentCore Gateway は、Lambda 関数や REST API、MCP サーバーなどを、AI エージェントが MCP(Model Context Protocol)で呼び出せるツールとしてまとめて公開するマネージドサービスです。2025 年 10 月の一般提供のころはツールのための MCP ゲートウェイでしたが、2026 年に入ってモデルの呼び出しを振り分ける inference ターゲット、エージェントへの呼び出しを中継する HTTP ターゲット、レート制限、Step Functions のコネクターなどが加わり、開発者ガイドも現在は Gateway をエージェント、ツール、モデルのための AI ゲートウェイと説明しています1。ツールを持たなければAIエージェントはただの推論エンジンです。AIエージェントが本番稼働する中で、「何かを繋げる」ためのエコシステムを統合的に扱うためのサービスとして拡張されてきたと言えるでしょう。 この記事では、公式ドキュメント、AWS ブログ、AWS Japan の Zenn 記事を横断して、Gateway のユースケースをやりたいことから引けるように、8 分類 38 件に整理してみました(もちろん、ここに示す以外のユースケースも多くあるはずです。ここに示すのはあくまで一つの例にすぎません)。情報は 2026 年 11 月 4 日時点のものです。 AgentCore Gateway の構成要素 Gateway の構成要素を 1 枚にまとめると、図 1 のようになります。 図1: AgentCore Gateway の構成要素 Gateway の背後に置く接続先をターゲットと呼び、表 1 のように MCP、HTTP、inference の 3 種類があります2。 ターゲット 主な接続先 Gateway が行うこと MCP ターゲット Lambda、API Gateway、OpenAPI、Smithy、MCP サーバー、SaaS の統合テンプレート、Web Search などの組み込みコネクター すべてのツールを 1 つの MCP サーバーとして公開し、有効にすればセマンティック検索のツールも加える HTTP ターゲット AgentCore Runtime のエージェント、A2A のエージェント、任意の HTTPS エンドポイント、AgentCore Memory のコネクター プロトコルを変換せず、パスで宛先を選んで中継する inference ターゲット Amazon Bedrock、OpenAI、Anthropic、OpenAI 互換のサービス リクエストの model の値でプロバイダーを選んで中継する 表1: ターゲットの種類 AgentCore Runtime は、エージェントや MCP サーバーをホストする AgentCore のサービスで、A2A はエージェント同士が連携するためのプロトコルです。また、MCP ターゲットのうち、既存の MCP サーバーをつなぐものを MCP サーバーターゲットと呼びます。呼び出し元は、MCP ターゲットのツールを /mcp で、HTTP ターゲットを /{ターゲット名}/ で始まるパスで3、inference ターゲットを /inference で始まるパスで呼び出します4。作成時にプロトコルの種類を MCP にした Gateway と、セマンティック検索を有効にした Gateway は MCP ターゲットしか持てないので、HTTP や inference のターゲットを使う Gateway は種類を指定せずに作ります5。 呼び出し元を確かめる Inbound 認証は、Amazon Cognito や外部の ID プロバイダー(IdP)が発行した JWT 形式のトークンか、IAM の認証情報で付けた SigV4 署名を検証します。認証だけを行う AUTHENTICATE_ONLY と、認証もしない NONE の方式もあり、この 2 つでは Policy、interceptor、ターゲットのどこかで認可します6。ターゲットを呼ぶときの Outbound 認証は、Gateway のサービスロール、OAuth、API キー、トークンのパススルーなどから、ターゲットの種類ごとに使える方式を選びます7。Inbound 認証と Outbound 認証のあいだに、ツールごとの許可を Cedar というポリシー言語で書く Policy、Lambda で判定や書き換えを挟む interceptor、呼び出し元やモデルごとのレート制限、HTTP と inference のターゲットへの振り分けとモデルのフォールバックを決める Gateway rules を組み合わせます。この記事では、どの部品を主に使うかで、ユースケースを表 2 の 8 つの分類に分けました。 分類 やりたいことの例 主に使う部品 1. 既存資産のツール化 既存の API や業務フローをエージェントに使わせたい MCP ターゲット、組み込みコネクター 2. MCP サーバーとツールの集約 散らばった MCP サーバーとツールを 1 か所で管理したい MCP サーバーターゲット、セマンティック検索、AWS Agent Registry 3. ツール呼び出しの統制 誰が何を呼べるかを決めて記録したい Inbound 認証、Policy、interceptor、レート制限 4. 利用者 ID の伝播とマルチテナント 利用者本人の権限で下流を呼び、テナントを分けたい Outbound 認証、interceptor 5. 閉域接続/ハイブリッド接続 VPC 内やオンプレミスの資産を公開せずに使わせたい VPC egress、インターフェイス VPC エンドポイント 6. モデル呼び出しの一元管理 利用者ごとにモデルと予算を管理したい inference ターゲット、Gateway rules、レート制限 7. エージェント呼び出しの集約 複数のエージェントへの呼び出しを 1 か所で管理したい HTTP ターゲット、Gateway rules 8. 長時間処理と人の確認 処理の途中で利用者に確認したい、長い処理を待ちたい elicitation、MCP Tasks、Step Functions のコネクター 表2: 8 つの分類と主に使う部品 なお、Gateway はエージェントとツールのあいだに必ず置くものではありません。AWS の設計ガイドである AWS Prescriptive Guidance は、ツールをエージェントのコードの中に持つ方法、リモートのツールに直接つなぐ方法、ツールゲートウェイを置く方法の 3 つを挙げ、統制の要件、ツールの数、運用の成熟度で選ぶよう勧めています8。実際、1 つのクライアントから AgentCore Runtime 上の 1 つの MCP サーバーを使うだけなら直結の方が構成も認証も単純で、AWS Japan の Zenn の検証では直結の方が中央値で約 0.94 秒速い結果でした9。 ! Gateway を置いても、エージェントやアプリがターゲットを直接呼べるなら、Gateway の認証や Policy は迂回されます。例えば、AgentCore runtimeにデプロイされたエージェントがAgentCore memoryにAgentCore gateway経由でアクセスできるように実装していても、runtime側でmemoryへのアクセス権限を持っているとgatewayをバイパスできる可能性があります。Runtime 側の送信元の制限や、ナレッジベースと Memory のリソースベースポリシーで Gateway 以外からの呼び出しを拒否する設定は、Gateway とセットで考えておく必要があります。 1. 既存資産のツール化 既存の API や Lambda 関数、業務フロー、SaaS、社内文書、Web 検索を、MCP サーバーを自分で作らずにツールとして使えるようにする分類です。Prescriptive Guidance は、業務ロジックが Lambda や既存の API にあるなら Gateway を先に検討し、独自の MCP サーバーが必要なときに AgentCore Runtime でホストするよう勧めています10。 既存の REST API を改修せずにツールにしたい OpenAPI 定義を OpenAPI ターゲットに登録すると、operation ごとにツールが作られ、Gateway が MCP の呼び出しを REST の呼び出しに変換します。オリックス株式会社様の文書管理 SaaS である PATPOST は、外部に公開していた REST API を改修せずに、PoC としてこの方法で MCP サーバー化しました。顧客エンゲージメント基盤を提供する MoEngage 様は、約 400 の API operation を 10 の社内エージェントに公開していて、各チームが OpenAPI 定義に注釈を書くと、CI がツールとして登録します。MCP サーバーを 20 以上作る案に比べ、約 6 か月分の開発工数を省いたと見積もっています。ただし、API を 1 対 1 でツールにすると呼び出しの往復が増えやすくなります。 参考 OpenAPI スキーマターゲット - Amazon Bedrock AgentCore:OpenAPI ターゲットの設定項目と制約 REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み:既存の API を改修せずに MCP サーバー化した事例 How MoEngage turned over 400 APIs into agent tools with AgentCore Gateway:約 400 の API をツールにした事例 API Gateway で管理している REST API を使わせたい API Gateway の REST API は、ステージ単位でターゲットにできます。API Gateway のコンソールからも追加でき、公開する操作はパスとメソッドの許可リストで絞り込みます。対象は同じアカウントかつ同じリージョンの REST API で、HTTP API と WebSocket API は使えません。エンドポイントタイプは public が前提なので、VPC 内のバックエンドへは、API Gateway の private integration でリクエストを転送します。 参考 ターゲットとしての Amazon API Gateway REST API ステージ - Amazon Bedrock AgentCore:対応する API の条件とツールの絞り込み Streamline AI agent tool interactions: Connect API Gateway to AgentCore Gateway with MCP:既存の REST API をターゲットにする手順 Lambda の業務ロジックをツールにしたい Lambda ターゲットでは、ツールのスキーマを Gateway に登録し、呼び出しのたびに Gateway が Lambda 関数を実行します。既存の関数も、Gateway が渡す入力の形式に合わせて手を入れれば使えるほか、細かい API をタスク単位にまとめ直したツールを作る場所にもなります。2025 年 11 月の AWS Japan の Zenn の検証では、AgentCore Runtime に置いた MCP サーバーより約 2 倍速く応答した一方、ツールの実装とスキーマが別々の場所に分かれ、ローカルで MCP サーバーとして試せない点が挙げられていました。 参考 AWS Lambda 関数のターゲット - Amazon Bedrock AgentCore:Lambda ターゲットとツールスキーマの書き方 Bedrock AgentCore で Remote MCP サーバーをホストする2つの方法の徹底検証:Runtime と Gateway + Lambda の比較 決まった手順で進む業務フローを呼ばせたい 2026 年 10 月に加わった Step Functions のコネクターを使うと、既存のステートマシンを、Lambda も MCP サーバーも書かずにツールにできます。エージェントが決めるのは実行のタイミングと入力だけで、手順の順序はステートマシンが決めるので、返金や各種の申請のように、決められた順序を守る必要がある業務に向いています。実行の開始や状況の確認など 10 の操作を公開できます。実行を開始する操作などは対象のステートマシンを作成時に固定でき、実行 ARN やタスクトークンを受け取る残りの操作は、サービスロールの IAM で範囲を絞ります。 参考 AWS Step Functions as Connector Target - Amazon Bedrock AgentCore:公開できる操作と、ステートマシンの固定の仕方 Step Functions のステートマシンを AgentCore Gateway 経由で MCP ツールとして呼び出してみた:コンソールで設定して呼び出すまでの手順 SaaS のデータや操作を使わせたい Salesforce、Slack、Jira、ServiceNow、Microsoft 365 系、Zendesk など 16 のプロバイダーの統合テンプレートが用意されていて、コンソールからターゲットに追加できます。使える API は、プロバイダーが提供する API の一部です。SaaS への認証情報は、エージェントの認証情報を管理する AgentCore Identity が保管します。テンプレートが受け付ける Outbound 認証は多くが API キーで、OAuth を受け付けるのは Salesforce、Zendesk、BambooHR です。Salesforce と SAP を 1 つの Gateway につなぐサンプルも公開されています。 参考 統合プロバイダーからの組み込みテンプレートをターゲットとして - Amazon Bedrock AgentCore:テンプレートごとの対応 API と認証方式 Multi-ISV Orchestration (Salesforce + SAP):複数の SaaS を 1 つの Gateway につなぐサンプル 社内文書を検索させたい 2026 年 6 月に一般提供された Managed Knowledge Base のコネクターを使うと、取り込み、ベクトルストア、検索までをマネージドにしたナレッジベースをツールにできます。公開されるのは、検索を計画して複数回実行し、引用付きの回答を返す AgenticRetrieveStream と、1 回のハイブリッド検索を行う Retrieve です。利用者ごとに検索範囲を分けるなら、interceptor で利用者の情報を引数に入れて Policy で検証し、ナレッジベースのリソースベースポリシーで Gateway 以外からのアクセスを拒否します。 参考 コネクタターゲットとしての Amazon Bedrock マネージドナレッジベース - Amazon Bedrock AgentCore:2 つのツールの入出力と設定 Bedrock Managed Knowledge Base を MCP Tool として利用する際のテナント分離・アクセス制御まとめ:利用者ごとに検索範囲を分ける方法 Web 検索を付けたい Web Search のコネクターは、Amazon の Web インデックスを使う検索をツールとして公開し、検索クエリは AWS の中で処理されます。料金は 1,000 クエリあたり 7 ドルで、2026 年 8 月からは東京リージョンでも使え、検索先のドメインや公開日で結果を絞り込めます。Claude Code on Bedrock のように組み込みの Web 検索を使えないクライアントに検索を足す使い方が、AWS Japan の Zenn で試されています。検索結果を使った回答を利用者に見せるときは、結果に付く出典とリンクを表示する必要があります。 参考 ウェブ検索ツール - Amazon Bedrock AgentCore:設定、絞り込み、対応リージョン Amazon Bedrock AgentCore での Web Search を発表:発表時のブログ Claude Code に AgentCore Gateway Web Search でインターネット検索を実装する:Claude Code から使う構成と料金の比較 2. MCP サーバーとツールの集約 エージェントやクライアントが増えると、MCP サーバーへの接続の設定がクライアントごとに散らばります。AWS ブログは、その状態では認証情報が各所に残り、ポリシーがクライアントごとにずれ、監査の記録が欠け、コストの計上先が分からなくなり、レビューを経ない連携が増えると整理しています11。 部門ごとの MCP サーバーを 1 つにまとめたい MCP サーバーをターゲットにすると、Gateway は複数のサーバーのツール、プロンプト、リソースを 1 つの MCP サーバーとして公開します。クライアントの接続先は 1 つで済み、各サーバーへの認証情報は Gateway の側で管理します。既定の設定では Gateway は同期したツールの定義を返すので、サーバー側でツールを変えたら同期の API を実行します。一覧を毎回サーバーから取る DYNAMIC の設定もありますが、セマンティック検索や authorization code の OAuth とは併用できません。Databricks や Dynatrace のように、ベンダーが提供する MCP サーバーも同じようにつなげます。 参考 MCP サーバーターゲット - Amazon Bedrock AgentCore:MCP サーバーターゲットの設定と同期 AgentCore Gateway の MCP サーバー統合による MCP アーキテクチャの変革:チームごとの MCP サーバーをまとめる例 ツール定義によるコンテキストの圧迫を減らしたい セマンティック検索を有効にすると、Gateway に x_amz_bedrock_agentcore_search というツールが加わり、エージェントは自然言語で必要なツールを探せます。見つかったツールだけをモデルに渡す処理は、クライアントの側で用意します。また、Policy で許可しないツールは tools/list にも出なくなります。MoEngage 様は 400 ツールの定義が 1 接続で 2 万トークンを超えると見積もり、クライアント側の絞り込みは認可にならないとして、Gateway の側で絞る構成を選びました。セマンティック検索を有効にした Gateway は MCP ターゲットしか持てず、検索の範囲もその Gateway の中に限られます。 参考 自然言語クエリを使用して AgentCore ゲートウェイ内のツールを検索する - Amazon Bedrock AgentCore:検索ツールの使い方と対応リージョン How MoEngage turned over 400 APIs into agent tools with AgentCore Gateway:Policy で一覧を絞る構成 社員のコーディングエージェントの MCP 設定を統制したい Kiro や Claude Code の mcp.json に、社内システムの認証情報が平文で残っている状態をなくす使い方です。Gateway の Inbound 認証を社内 IdP の SSO にし、バックエンドの認証情報は AgentCore Identity に保管し、呼び出しは CloudTrail に記録します。AWS ブログは、2 人のアナリストが Amazon Quick から使い始め、6 か月で 1,000 人に広げた金融機関を参考の導入例として、接続、統制、カタログ化、堅牢化の 4 段階で紹介しています。mcp.json は端末管理のツールで配り、Gateway 以外の MCP サーバーへの通信は社内のプロキシや EDR で止めておきます。 参考 Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:4 段階で統制を広げる構成と導入の経過 ゲートウェイのインバウンド認可を設定する - Amazon Bedrock AgentCore:JWT と IAM の Inbound 認証の設定 社内のツールを探して再利用させたい AWS Agent Registry は、エージェント、MCP サーバー、ツール、スキルを登録し、承認を経たものだけを検索できるようにするカタログです。2026 年 8 月に一般提供され、AWS Organizations の各アカウントにある AgentCore Runtime と Gateway を自動で検出して登録できるので、組織のアカウントに作られたまま管理者が把握していない Runtime や Gateway を見つける手段にもなります。見つけたツールは自分の Gateway のターゲットに加え、Policy で呼び出しを許可して使います。 参考 AWS エージェントレジストリ: エージェント、ツール、リソースを検出して管理する - Amazon Bedrock AgentCore:Registry の機能と使い方 Kiro に野良 MCP を繋がせない。AWS Agent Registry × Kiro for Enterprise で配布を統制する:Kiro と組み合わせて配布を統制する検証 Amazon Quick から社内システムを使わせたい 業務向けの AI アシスタントである Amazon Quick の MCP 連携から Gateway につなぐと、Lambda のツールを経由して、VPC 内の基幹 DB を公開せずに業務部門のチャットから使えます。Quick は 1 つの MCP 接続に 100 件までしかツールを登録せず、サービス間の OAuth では Quick の利用者個人を Policy で識別できません。業務ドメインごとに Gateway を分け、利用者ごとの認可が必要ならユーザー認証の方式を検討します。Runtime 上の MCP サーバー 1 つを使うだけなら、Gateway を挟まない直結も選べます。 参考 Amazon Quickから基幹システムのRDSを使う!AgentCore Gatewayと関数型MCPツールで作るリファレンス構成:基幹 DB を公開せずに使う構成とツールの設計 AgentCore Gateway をどう設計するか。MCP の集約・検索・認可を Amazon Quick で検証:直結と Gateway 経由の比較 Connect an AgentCore Runtime hosted MCP server to Amazon Quick:Runtime の MCP サーバーを Quick につなぐ手順 3. ツール呼び出しの統制 誰がどのツールを呼べるかを決め、呼び出しを記録する分類です。アクセス制御の主な手段は Inbound 認証、Policy、interceptor の 3 つで、Policy がプレビューだった 2026 年 1 月の AWS Japan の Zenn の測定では、50 回の平均で、Inbound 認証だけの構成に比べて tools/list の応答が Policy で約 134 ミリ秒、interceptor で約 207 ミリ秒増えました12。測定した記事は、ツールを何度も呼ぶエージェントでは差の積み重なりを考える必要があるものの、柔軟な認可を考えれば許容できる範囲だと結論しています。 部署やロールごとに使えるツールを変えたい Policy は、Cedar で書いたポリシーを Gateway への呼び出しごとに評価し、許可されたツールの呼び出しだけを通します。既定は拒否で、LOG_ONLY のモードで影響を確かめてから ENFORCE に切り替えられ、ポリシーの案は自然言語からも作れます。JWT のクレームで部署やグループを見分けられ、条件にはツールの引数も使えるので、デプロイのツールを staging の環境に限って許可する、といった書き方もできます。Inbound 認証だけの構成では、認証を通った呼び出し元がすべてのツールを呼べます。 参考 Amazon Bedrock AgentCore のポリシー: エージェントのインタラクションを制御する - Amazon Bedrock AgentCore:Policy の機能と使い方 AgentCore Gateway で MCP サーバー・ツールのアクセス制御を実現する 3 つの方法の徹底比較:Inbound 認証、Policy、interceptor の使い分け 外部の権限情報や複雑な条件で判定したい interceptor は、Gateway がターゲットを呼ぶ前と後に実行される Lambda 関数で、外部の DB を参照した判定や、引数と結果の書き換えができます。Cedar のポリシーは呼び出しに含まれる情報だけで判定するので、interceptor が属性を取ってきて引数に入れ、Policy がその値で判定する組み合わせが AWS ブログで紹介されています。この例では、EU の利用者には個別の請求記録を見せないという規則を、DynamoDB に置いた利用者の地域と Cedar の forbid で実装しています。 参考 Gateway でのインターセプターの使用 - Amazon Bedrock AgentCore:interceptor の種類と設定 Secure AI agents with Policy and Lambda interceptors in Amazon Bedrock AgentCore gateway:Policy と interceptor の使い分けと組み合わせ Amazon Bedrock AgentCore ゲートウェイインターセプター: きめ細かなアクセス制御の実現:ツール一覧の絞り込みや ID の伝え方の例 承認の後でしか実行させたくない temporal policy は、同じセッションで過去に行われた操作を条件にできるポリシーです。承認を確かめる操作が成功していなければ送金を拒否する、ある操作の回数や累計額に上限を設ける、といった、1 回の呼び出しだけでは決められない規則を Gateway で判定できます。ここでのセッションは、呼び出し元が x-amzn-bedrock-agentcore-policy-session-id ヘッダーで渡す policy session で、回数や累計額もそのセッションの中だけで数えます。ポリシーは Cedar と互換の Dogwood で書き、LOG_ONLY のモードで試せます。 参考 一時ポリシー - Amazon Bedrock AgentCore:temporal policy の書き方 Temporal policies のサンプル:銀行のアシスタントを題材にしたサンプル ツールの入出力から個人情報を除きたい Policy の Guardrails 連携を使うと、ターゲットへの入力やツールの結果を Amazon Bedrock Guardrails で検査し、個人情報やプロンプトインジェクションの疑いを条件に拒否できます。検査はエージェントのコードの外で行われるので、エージェントが推論で迂回することはできません。値をマスクしたい場合は、モデルを呼ばずにガードレールの判定だけを行う ApplyGuardrail API を interceptor から呼ぶ方法があり、AWS Japan の Zenn では、マイナンバーを引数と結果の両方でマスクできることを確かめています。番号を登録するためのツールでは、引数をマスクすると番号を登録できなくなるので、結果だけをマスクするよう分ける必要がありました。 参考 ポリシーのガードレール - Amazon Bedrock AgentCore:Guardrails の判定をポリシーの条件にする方法 MCP ツールの引数と結果に入った個人番号を、AgentCore Gateway のインターセプターでマスクする:interceptor でのマスクと制約 汎用の MCP クライアントから利用者のログインで使わせたい MCP クライアントの中には、動的クライアント登録(DCR)で自分を OAuth のクライアントとして登録してから接続するものがあります。IdP が DCR に対応していれば、その IdP を Gateway の JWT の Inbound 認証に使う構成を取れます。対応していない場合は、API Gateway と Lambda で登録を受け付けて Cognito のアプリクライアントを作り、Gateway の許可リストに加える仲介を置く構成が AWS ブログで紹介されています。DCR に対応した Auth0 を使う構成は、AWS Japan の Zenn で試されています。 参考 AgentCore Gateway + Auth0を利用して、DCR可能なリモートMCPサーバーを構築してみた:Auth0 で DCR に対応させる構成 Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:Cognito を使う DCR の仲介の構成 一部の利用者の使いすぎを抑えたい 2026 年 8 月から、Gateway に自分でレート制限を設定できるようになりました。JWT のクレームや IAM の身元、ターゲット、ツール、モデルの組み合わせごとに、1 秒あたりや 1 分あたりのリクエスト数、推論の 1 分あたりのトークン数、同時接続数の上限を置けます。複数の制限はすべてを満たす必要があります。上限を 0 にすると特定の呼び出し元を止められますが、反映に最大 30 秒かかり、判定できないときは呼び出しを通すので、確実に遮断するなら認証や Policy で拒否します。レート制限は Policy より先に評価されるので、Policy で拒否される呼び出しも回数に数えられます。追加料金はかかりません。 参考 ゲートウェイにレート制限を追加する - Amazon Bedrock AgentCore:制限の単位と設定 Configure rate limits for AI traffic on AgentCore gateway:ロールや利用者ごとの制限を組み合わせる例 誰が何を呼んだかを監査で説明したい Gateway への呼び出しは CloudTrail のデータイベントとして記録でき、CloudWatch にはログとトレース、Policy の許可と拒否の判定が残ります。AWS ブログの金融機関の例では、どのアナリストがどのポジションを、どのポリシーの下で照会したかを規制当局がたどれる状態にしています。利用者の単位で説明するには、サービス間の認証ではなく、利用者のトークンで Gateway を呼ぶ構成にしておく必要があります。 参考 CloudTrail を使用した Amazon Bedrock AgentCore Gateway API コールのログ記録 - Amazon Bedrock AgentCore:データイベントの有効化と記録の内容 AgentCore が生成したゲートウェイオブザーバビリティデータ - Amazon Bedrock AgentCore:メトリクス、ログ、トレース 4. 利用者 ID の伝播とマルチテナント 人が画面から操作するときはアプリが利用者を把握していますが、エージェントが利用者の代わりに API を呼ぶと、いま誰が操作しているかという情報が途中で失われやすくなります13。この分類では、利用者の ID を下流の API まで渡すことと、テナント ID のようなアクセス制御に使う値を、LLM が組み立てるツールの引数にしないことの両方が求められます。 利用者本人の同意で SaaS 上のデータを扱わせたい Outbound 認証に authorization code の OAuth を使うと、Gateway は利用者ごとのトークンで SaaS を呼び出します。利用者がブラウザで一度同意すると、トークンは AgentCore Identity が利用者ごとに保管し、以降の呼び出しに使います。MCP ターゲットでの対応は 2026 年 4 月に一般提供され、9 月には同意画面を用意する Consent Portal も AgentCore Identity に加わりました。Consent Portal を使うには、JWT で Inbound 認証をする Gateway が必要です。 参考 ゲートウェイのアウトバウンド認可を設定する - Amazon Bedrock AgentCore:ターゲットの種類ごとに使える認証方式 同意ポータルを設定する - Amazon Bedrock AgentCore:Consent Portal の設定 利用者の ID を下流の API まで渡したい Outbound 認証の token exchange を使うと、Gateway は受け取った利用者のトークンを、下流の API 向けに範囲を絞ったトークンに交換してから呼び出します。交換後のトークンには利用者とエージェントの両方の ID が入るので、下流の API は追加の同意なしに利用者の単位で認可できます。受け取ったトークンをそのまま下流に渡すと権限の流用につながり、MCP の認可仕様もこれを禁じています。Gateway でトークンをそのまま渡す方式を選べるのは HTTP ターゲットだけで、token exchange は MCP サーバーと OpenAPI のターゲットで使えます。 参考 AgentCore Identity とのトークン交換On-behalf-of - Amazon Bedrock AgentCore:token exchange の仕組みと設定 Implement on-behalf-of token exchange for multi-tenant agents with Amazon Bedrock AgentCore Gateway:マルチテナントでの実装例 テナントごとにデータを分けたい 全テナントで同じエージェントとツールを使いながらデータを分けるには、REQUEST interceptor で ID トークンからテナントを読み取り、テナントの一時認証情報かテナント ID をツールに渡します。分離はデータベースの側で強制し、AWS Japan の Zenn では、Cognito の ID プールで交換した一時認証情報を Lake Formation で使う分離と、テナント ID で Aurora PostgreSQL のロールを切り替えて行レベルセキュリティを適用する分離を確かめています。どちらの例も、テナント ID を LLM が作る引数にしていません。 参考 AgentCore Gateway でマルチテナント環境における Agent のデータアクセス制御 (FGAC) を実現する:Lake Formation で分離する構成 AI エージェントの DB アクセスを AgentCore Gateway + PostgreSQL のネイティブ権限で制御する:PostgreSQL の権限で分離する構成 Gateway によるヘッダー伝達 - Amazon Bedrock AgentCore:ターゲットにヘッダーを渡す設定 利用者ごとの API キーで動く SaaS を使わせたい 利用者ごとに API キーを発行している既存の SaaS では、interceptor の中で利用者の API キーを付けてから OpenAPI ターゲットに渡す方法があります。オリックス株式会社様の PATPOST はこの形で、すでに使っていた Cognito と利用者ごとの API キーを活かし、本体に手を入れずに利用者ごとにデータを出し分けました。API キーがエージェントの実行環境やモデルのコンテキストに渡らないので、プロンプトインジェクションで API キーが漏れることへの対策になります。 参考 REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み:認証情報の渡し方とツールの設計の議論 Gateway でのインターセプターの使用 - Amazon Bedrock AgentCore:ヘッダーを interceptor に渡す設定 エンドユーザーに Memory を本人の分だけ使わせたい エージェントの記憶を管理する AgentCore Memory のデータプレーンは SigV4 の認証だけですが、Memory のコネクターで Gateway の背後に置くと、エンドユーザーが OAuth のトークンで Memory を呼べるようになり、アプリに AWS の認証情報を配らずに済みます。Memory のコネクターは HTTP ターゲットで、MCP のツールとしては公開されず、Memory の API のパスで呼び出します。Memory の記録はユーザーを表す actor と、保存先の区分である namespace で分かれるので、Policy で本人の actor と namespace への呼び出しだけを許可し、Memory のリソースベースポリシーで Gateway 以外からの呼び出しを拒否します。 参考 ゲートウェイ経由で AgentCore Memory にアクセスする - Amazon Bedrock AgentCore:Memory のコネクターと認証の方式 AgentCore Memory connector と FGAC でメモリーアクセスを統制する:本人の記録だけを使わせる構成の検証 自社のツールを顧客の AWS アカウントに提供したい AWS Builder Center の記事は、ソフトウェアを提供する事業者(ISV)が自社の MCP サーバーを複数の顧客に提供する構成をどこにgatewayを置くかで分けた 2 つ紹介しています。ISV が Gateway を持ち、顧客は PrivateLink で呼び出し、Policy でテナントごとに使えるツールを分ける構成と、顧客が自分の Gateway を持ち、ISV の MCP サーバーを VPC Lattice のリソース設定として AWS RAM で共有してもらう構成です。後者は Transit Gateway や VPC ピアリングなしで組めます。前者では、顧客の VPC 以外からの呼び出しを Gateway のリソースベースポリシーで拒否します。図は、ISV が Gateway を持つ前者の構成です。 参考 Networking Patterns for ISVs Distributing AI Agent Tools via Amazon Bedrock AgentCore Gateway:2 つの構成の手順(AWS Builder Center)。プライベート CA の記述は 2026 年 10 月より前のもの Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:VPC Lattice と AWS RAM による接続 5. 閉域接続/ハイブリッド接続 規制のある業種や基幹システムでは、ツールを公開エンドポイントに置けない場合があります。Gateway からターゲットへ閉域でつなぐ方式は、ターゲットの種類によって異なります14。 VPC 内の MCP サーバーや API を公開せずに使わせたい 2026 年 4 月に加わった VPC egress では、MCP サーバーと OpenAPI のターゲットに private endpoint を設定し、VPC の中に置いた VPC Lattice のリソースゲートウェイを通して、EKS や ECS のサーバーにリクエストを送ります。リソースゲートウェイは、Gateway が作成して管理する方式と、自分で作る方式から選べます。10 月からは社内 CA の証明書を信頼の起点として登録でき、ALB を挟まずに接続できます。Lambda のターゲットは関数を VPC 内で実行し、API Gateway のターゲットは VPC Link で VPC 内につなぎます。Gateway から自分の VPC への通信には、1 GB あたり 0.006 ドルのデータ処理料金がかかります。 参考 ゲートウェイターゲットの Amazon Bedrock AgentCore Gateway VPC Egress を設定する - Amazon Bedrock AgentCore:private endpoint とプライベート CA の設定 Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:ターゲットの種類ごとの接続方法 オンプレミスや他クラウドのシステムを使わせたい VPC egress の先を Direct Connect や Site-to-Site VPN につなげば、オンプレミスや他のクラウドで動く MCP サーバーや REST API もターゲットにできます。エージェントからは、ターゲットが AWS の上にあるかどうかは見えません。AWS ブログの金融機関の例では、トレーディングのシステムをオンプレミスに残したまま、Direct Connect 経由でツールとして使っています。 参考 Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:オンプレミスや他クラウドへの接続 Govern AI agent tool access with Amazon Bedrock AgentCore Gateway:オンプレミスのシステムをツールにした例 別アカウントのツールを使わせたい ツールを提供するアカウントで、VPC で通信を受け入れる VPC Lattice のリソースゲートウェイと、MCP サーバーを指すリソース設定を作り、AWS RAM で利用する側のアカウントに共有します。利用する側の Gateway で、自分で作る方式の private endpoint にそのリソース設定を指定すると、AgentCore がリソース設定を AgentCore の VPC Lattice のサービスネットワークに関連付けます。Gateway からのリクエストはこのサービスネットワークとリソースゲートウェイを通って VPC に入るので、インターネットを経由せず、VPC ピアリングや Transit Gateway も要りません。 参考 Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets:自分で管理する VPC Lattice によるアカウント間の接続 VPC Lattice を使用して VPC 内のプライベートリソースに接続する - Amazon Bedrock AgentCore:リソースゲートウェイ、リソース設定、サービスネットワークへの関連付けの仕組み エージェントからも閉域で Gateway を呼びたい インターフェイス VPC エンドポイントを作ると、VPC 内のエージェントは PrivateLink 経由で Gateway を呼べます。プライベート DNS を有効にすれば、Gateway の通常の URL のまま使えます。ただし、エンドポイントを作るだけでは公開側からの呼び出しは止まらないので、VPC からに限るには Gateway のリソースベースポリシーで aws:SourceVpc などを条件にします。VPC エンドポイントポリシーで呼べる Gateway を絞れますが、ポリシーの変更が反映されるまでに最大 15 分かかります。2026 年 3 月からは、Gateway の作成や変更のための API も PrivateLink で呼べます。 参考 インターフェイス VPC エンドポイント (AWS PrivateLink) を使用して、VPC と Amazon Bedrock AgentCore リソース間のプライベート接続を作成する - Amazon Bedrock AgentCore:VPC エンドポイントの作成 Secure ingress connectivity to Amazon Bedrock AgentCore Gateway using interface VPC endpoints:VPC エンドポイントポリシーと構成の例。コントロールプレーンの PrivateLink 対応より前の記事 Amazon Bedrock AgentCore のリソースベースのポリシー - Amazon Bedrock AgentCore:aws:SourceVpc などで呼び出し元の VPC を絞る例 6. モデル呼び出しの一元管理 2026 年 6 月に inference ターゲットが加わってできた分類です15。ツールの呼び出しに使っている Inbound 認証、Policy、レート制限を、同じ Gateway でモデルの呼び出しにも使えます。 複数のプロバイダーのモデルを 1 つのエンドポイントで使い分けたい inference ターゲットを使うと、Gateway は OpenAI や Anthropic の API と互換のエンドポイントになり、リクエストの model の値で Amazon Bedrock、OpenAI、Anthropic などに振り分けます。クライアントは OpenAI SDK や Anthropic SDK をそのまま使い、接続先と認証情報を Gateway 向けに変えればよく、プロバイダーの認証情報は Gateway の側に置けます。Amazon Bedrock、OpenAI、Anthropic は connector を選んで名前を指定するだけで追加でき、ほかのサービスは provider でエンドポイントとモデルの対応を書きます。10 月からは InvokeModel の形式でも呼べますが、ペイロードの形式は変換しないので、本文は宛先と同じ形式にします。 参考 推論ターゲット - Amazon Bedrock AgentCore:connector と provider の設定 Use the Amazon Bedrock InvokeModel API with inference targets - Amazon Bedrock AgentCore:InvokeModel の形式との変換 利用者ごとにモデルの権限、量、予算を管理したい エージェントがモデルを繰り返し呼ぶ使い方を想定して、モデルの利用を利用者ごとに管理する構成が、AWS Japan の Zenn で公開されています。Policy で利用者ごとに使えるモデルを決め、レート制限で 1 分あたりのトークン数と同時接続数を絞り、REQUEST と RESPONSE の interceptor が DynamoDB に記録した費用を日次と月次の予算と比べます。費用は応答のあとに計上するので、予算の超過を厳密には防げませんが、予算の管理やチームごとの支出の集計が要件の中心なら、それらを主な機能にしている LiteLLM のような AI ゲートウェイも比較の候補になります。 参考 Amazon Bedrock AgentCore Gateway によるモデル利用の認証・認可と利用量・予算管理:構成とサンプルのリポジトリ Envoy AI Gateway は Agent Router になった。モデルと MCP の通信ポリシーを Envoy に集約する:LiteLLM や Agent Router(旧 Envoy AI Gateway)との使い分け モデルの障害やスロットリングに備えたい 2026 年 10 月から、Gateway rules にモデルのフォールバックを書けるようになりました。順番に試す代わりの宛先と、切り替える失敗の種類、最初の 1 バイトが返るまでのタイムアウトを指定しておくと、第 1 候補がスロットリングや容量不足で応答しないときに、次の候補へ自動で切り替わります。いま失敗している宛先は、既定で有効なサーキットブレーカーが避け、次の宛先に送ります。応答のストリーミングが始まった後の失敗は切り替えられずクライアントで再試行しますが、それより前の失敗については、クライアントごとに切り替えを実装しなくて済みます。 参考 Model routing and fallback - Amazon Bedrock AgentCore:フォールバックのルールの書き方 AgentCore Gateway now supports Bedrock InvokeModel APIs and model fallback:発表時の What's New 7. エージェント呼び出しの集約 2026 年 6 月に HTTP ターゲットが加わり、ツールだけでなくエージェント自体への呼び出しも Gateway で受けられるようになりました15:1。 複数のエージェントに共通の認証と監査をかけたい AgentCore Runtime のエージェントをターゲットにすると、/sales/invocations のように先頭のターゲット名で宛先を振り分け、Gateway の認証、Policy、ログを共通で使えます。Runtime の側では、IAM の場合は aws:SourceArn の条件、JWT の場合は許可するワークロードの設定で、Gateway 以外からの呼び出しを拒否できます。また、Runtime ターゲットを置く Gateway は、プロトコルの種類を指定せずに作ります。 参考 Amazon Bedrock AgentCore ランタイムターゲット - Amazon Bedrock AgentCore:Runtime ターゲットの制約と、Gateway 以外の呼び出しの拒否 外部の A2A エージェントや独自の HTTP サービスを並べたい HTTP passthrough のターゲットは、任意の HTTPS のエンドポイントを Gateway の背後に置き、認証、Policy、ログを共通にします。A2A のエージェントや、単独で公開したい MCP サーバー、独自の推論エンドポイントが対象です。protocolType に A2A、MCP、INFERENCE、CUSTOM のいずれかを指定し、Gateway はその種類をログと Policy の判定に使いますが、プロトコルの変換はしません。API のスキーマを渡すと Policy の判定やガードレールを適用でき、CUSTOM でガードレールを使う場合はスキーマが必須です。 参考 HTTP パススルーターゲット - Amazon Bedrock AgentCore:passthrough の設定と対応する HTTP メソッド 新しいプロンプトやエージェントを本番の利用で比べたい エージェントの改善を支援する AgentCore の optimization の A/B テストでは、Gateway がトラフィックをセッションごとに 2 つの版へ分け、評価の機能である AgentCore Evaluations で採点して、差が統計的に有意かどうかを確かめます。プロンプトやモデルの違いなら同じ Runtime の設定の版を分け、実装の違いなら別のエンドポイントを指す 2 つのターゲットに振り分けます。AgentCore の外で動くエージェントも HTTP ターゲットにすれば比べられ、勝った版への切り替えは Gateway rules で行います。1 つの Gateway に設定できる A/B テストは 1 つです。 参考 AgentCore 最適化: レコメンデーションと A/B テストによりエージェント品質ループを改善 - Amazon Bedrock AgentCore:A/B テストの仕組み ゲートウェイにルールを追加する - Amazon Bedrock AgentCore:ルールの評価の順序と重み付きの振り分け 8. 長時間処理と人の確認 2026 年 5 月以降、Gateway は MCP サーバーとクライアントのあいだの双方向のやり取りを中継できるようになりました15:2。処理の途中で利用者に入力や確認を求める機能である elicitation と、時間のかかる処理の結果を後から受け取る機能である MCP Tasks を使えるのは、MCP サーバーターゲットだけです。 実行の途中で利用者に確認を求めたい elicitation は、MCP サーバーがツールの実行中に利用者へ入力や確認を求める MCP の機能で、Gateway はその要求をクライアントに中継し、回答を元のサーバーに返します。返金を実行する前の確認のようなフォームと、OAuth の同意画面へ誘導する URL の 2 つの形に対応しています。2025-11-25 版までのプロトコルでは MCP セッションとレスポンスのストリーミングを有効にする必要があり、2026-07-28 版では不要です。 参考 AgentCore ゲートウェイで誘発を使用する - Amazon Bedrock AgentCore:elicitation の前提と動き Extending MCP support for Amazon Bedrock AgentCore Gateway:セッション、ストリーミング、elicitation の追加 15 分を超える処理をツールにしたい Gateway の同期の呼び出しのタイムアウトは既定で 15 分で、引き上げを申請できます。2026 年 10 月に対応した MCP の Tasks の拡張では、ツールがすぐにタスクの ID を返し、クライアントが tasks/get で状態と結果を取りに行くので、バッチ処理やレポートの作成のような長い処理もツールにできます。途中で入力を求められたら tasks/update で答え、tasks/cancel で取り消しを依頼できます。プロトコルの 2026-07-28 版が前提で、JWT か IAM で認証する Gateway では、タスクは作成した呼び出し元に紐づきます。 参考 Use tasks with your AgentCore gateway - Amazon Bedrock AgentCore:Tasks の前提と動き How AgentCore Gateway supports the MCP 2026-07-28 spec:2026-07-28 版への移行の考え方 数日かかる承認待ちを含む業務と組み合わせたい Step Functions の Standard ワークフローは最長 1 年動き、コールバックのタスクで外部からの完了の連絡を待てます。数日かかる承認はワークフローの側で待ち、承認者への依頼と受け付けもワークフローから行います。エージェントはコネクターの StartExecution で業務を始め、結果は後のセッションで DescribeExecution を呼んで確かめます。途中の作業をエージェントが行う場合は、ワークフローがタスクトークンを SQS などでエージェントのアプリに渡し、作業を終えたエージェントが SendTaskSuccess を呼んで再開させます。どちらの場合も待つのはワークフローなので、Runtime のセッションを承認が終わるまで動かし続ける必要はありません。有効なタスクトークンがあればそのタスクを完了できるので、トークンはクレデンシャルと同じように扱います。 参考 AWS Step Functions as Connector Target - Amazon Bedrock AgentCore:コールバックで使う操作と注意点 アップデート AWS Step FunctionsのワークフローをAgentCore Gateway経由でAIエージェントのツールとして使えるようになりました!:承認の待機を Amazon SNS とタスクトークンで実装した例 Asynchronous patterns for calling Amazon Bedrock AgentCore agents in serverless pipelines:ワークフローがエージェントを非同期に呼び出し、エージェントが SendTaskSuccess で再開させる例 まとめ この記事で取り上げた PATPOST と MoEngage 様の事例、AWS ブログの金融機関の例は、いずれも既存の API のツール化や MCP の設定の集約から始まっていますが、設計の議論では利用者ごとの認可や監査、閉域での接続が大きな論点になっていました。Gateway を使い始めるきっかけは MCP サーバーを作らずにツールを公開できることでも、置き続ける理由になるのは、誰がどの権限で何を呼べるかを 1 か所で決め、その呼び出しを記録できることの方だと考えています。 ただし、Gateway での認可と記録は、Runtime の送信元の制限やリソースベースポリシーで Gateway を通らない呼び出しを拒否するか、下流の認証情報を Gateway だけに持たせて、初めて成り立ちます。また、1 つのクライアントから 1 つの MCP サーバーを使うだけなら直結で十分なので、つなぐツールや利用者が増えた段階で Gateway を検討してみてください。 脚注 Amazon Bedrock AgentCore Gateway: エージェント、ツール、モデル用の安全な AI ゲートウェイ - Amazon Bedrock AgentCore ↩︎ Amazon Bedrock AgentCore Gateway の主な概念 - Amazon Bedrock AgentCore ↩︎ HTTP パススルーターゲット - Amazon Bedrock AgentCore ↩︎ 推論コネクタターゲット - Amazon Bedrock AgentCore ↩︎ API を使用して AgentCore ゲートウェイを作成する - Amazon Bedrock AgentCore ↩︎ ゲートウェイのインバウンド認可を設定する - Amazon Bedrock AgentCore ↩︎ ゲートウェイのアウトバウンド認可を設定する - Amazon Bedrock AgentCore ↩︎ Core services: tools - AWS Prescriptive Guidance ↩︎ AgentCore Gateway をどう設計するか。MCP の集約・検索・認可を Amazon Quick で検証 ↩︎ Deployment Pattern 1 - Amazon Bedrock AgentCore - AWS Prescriptive Guidance ↩︎ Govern AI agent tool access with Amazon Bedrock AgentCore Gateway ↩︎ AgentCore Gateway で MCP サーバー・ツールのアクセス制御を実現する 3 つの方法の徹底比較 ↩︎ REST API を Amazon Bedrock AgentCore Gateway で MCP サーバー化する ― オリックス「PATPOST」での取り組み ↩︎ Private connectivity patterns for Amazon Bedrock AgentCore Gateway Targets ↩︎ Amazon Bedrock AgentCore のリリースノート - Amazon Bedrock AgentCore ↩︎ ↩︎ ↩︎
The Most Important AI Engineering Skill Is Saying No (AI-DLC, bolts, and autonomous agents) --- title: "The Most Important AI Engineering Skill Is Saying No (AI-DLC, bolts, and autonomous agents)" published: true description: "AI-DLC, bolts, and autonomous agents promise faster software delivery. But who owns the architecture when AI starts making the decisions?" tags: ai, softwareengineering, architecture, productivity --- AI can now propose an architecture, break requirements into tasks, write the implementation, generate tests, and prepare a deployment. And we are increasingly building entire development methodologies around that capability. AWS introduced the AI-Driven Development Lifecycle (AI-DLC), with concepts such as Units of Work, Bolts, Mob Elaboration, and Mob Construction. Frameworks such as specs.md bring similar ideas into tools like Claude Code. The promise is compelling: shorter development cycles, persistent context, specialized agents, and fewer manual handoffs. But after exploring these methodologies, I keep returning to a question that seems more important than how quickly AI can produce software: Who is actually making the engineering decisions? Not who approves the final pull request. Not who clicks "continue" when an agent requests permission. Who decides whether an architectural proposal makes sense in the first place? {% card %} My argument: The most valuable human contribution to AI-driven software development may be the ability to reject an AI proposal, establish constraints, demand alternatives, and stop execution. If developers lose that authority, human-in-the-loop becomes little more than a ceremonial approval process. {% endcard %} We are redesigning software development around AI For years, the software industry has experimented with methodologies intended to improve how teams deliver software. Waterfall emphasized sequential phases. Agile introduced iterative development and faster feedback. Scrum provided a framework for organizing complex work, while Kanban emphasized managing flow. Now we have AI-native development methodologies. AWS describes AI-DLC as a model in which AI creates plans, asks questions, proposes solutions, and executes work after human validation. Its lifecycle has three major phases: | Phase | AI responsibility | Human responsibility | |---|---|---| | Inception | Elaborate requirements and propose units | Validate business intent and scope | | Construction | Propose designs, generate code, and test | Make technical decisions and validate results | | Operations | Prepare infrastructure and operational changes | Provide oversight and authorize consequential actions | The methodology also introduces Bolts, short execution cycles generally measured in hours or days rather than weeks. The ambition is to reorganize the development lifecycle around AI's ability to generate and execute work rapidly. {% embed aws.amazon.com %} This is a legitimate direction to explore. But there is a subtle difference between accelerating software development and accelerating the production of software artifacts. An agent can produce five architectural alternatives in minutes. It cannot guarantee that any of them is appropriate for the organization, its operational constraints, or the business problem. That distinction becomes increasingly important as agents gain autonomy. A Bolt is not a substitute for engineering judgment Consider a traditional development team implementing a reservation management feature. A developer might spend time understanding the domain, inspecting existing services, discussing edge cases, and choosing an implementation strategy. With an AI-driven workflow, an agent may perform much of that investigation and propose an implementation plan. The team can then approve the plan and let the agent execute it. This sounds efficient. But consider what happens when the agent proposes an unnecessarily complicated solution. For example: > Introduce a Kafka topic to propagate reservation state changes, a Redis cache for availability lookups, and a new microservice responsible for reservation lifecycle management. It sounds like an architecture. It even sounds like a reasonably sophisticated one. But what if the system currently handles a few hundred reservations per day, uses a single PostgreSQL database, and has no demonstrated scaling problem? The correct engineering decision might be: No. We do not need any of that. We might need one additional endpoint, a transaction, a database constraint, and two integration tests. An AI agent can generate a technically coherent architecture that is nevertheless a poor engineering decision. The danger is not only incorrect code. It is unnecessary complexity delivered at extraordinary speed. {% card %} A successful Bolt should not be measured by how much work the agent completed. It should be measured by whether the team delivered a correct, maintainable increment that solves the intended problem. {% endcard %} Human-in-the-loop is not the same as human-in-control This distinction deserves more attention. Many AI workflows include human checkpoints. The agent presents a plan, and a developer approves or rejects it. That is useful, but insufficient. Imagine the following interaction: AI: I recommend introducing an event-driven architecture. Developer: Approved. AI: I will implement three services and their supporting infrastructure. Developer: Approved. AI: Implementation complete. All generated tests pass. Developer: Approved. There were three human checkpoints. Yet we have no evidence that meaningful engineering judgment occurred at any of them. Now consider a different interaction. AI: I recommend introducing an event-driven architecture. Developer: Why? What problem does it solve that our existing transactional design cannot? AI: It could improve decoupling and asynchronous processing. Developer: Show me three alternatives. Compare operational cost, consistency guarantees, failure recovery, and migration effort. Our team maintains this platform with limited infrastructure capacity. AI: A transactional implementation appears sufficient under those constraints. Developer: Proceed with that approach. Do not introduce new infrastructure. The second interaction may produce less code. It may also produce substantially better software. The developer was not merely validating output. The developer was actively shaping the solution. Human-in-the-loop describes participation. Human-in-control describes authority. They are not equivalent. The four powers developers must retain I believe every serious AI engineering workflow should explicitly preserve four human capabilities. 1. Reject a proposal Developers must be able to reject a design without having to provide a replacement implementation immediately. This matters because rejecting a bad abstraction is often easier than constructing a better one. An AI-generated design may be logically consistent but violate established architectural boundaries, introduce unnecessary dependencies, or optimize for a problem that does not exist. A useful rejection should identify the concern: > Reject this proposal. It introduces a new service without demonstrating a need for independent scaling, deployment, or domain ownership. That is an engineering decision. Not an obstacle to automation. 2. Establish constraints AI agents need boundaries. Not just coding conventions, but architectural and operational constraints. For example: architecture: new_microservices: prohibited database: postgresql messaging: existing_kafka_topics_only public_api_breaking_changes: prohibited reliability: retries_must_be_bounded: true idempotency_required_for_commands: true backward_compatibility: required delivery: unrelated_refactoring: prohibited integration_tests: required production_deployment: manual_approval This is an illustrative policy, not a native specs.md configuration file. The point is that constraints should be explicit, versioned, and testable wherever possible. A developer should not need to repeat the same architectural principles every time a new agent session begins. 3. Demand alternatives One of the most dangerous properties of AI-generated solutions is how persuasive they can appear. A complete design document with diagrams, tradeoff tables, and confident explanations can create the impression that the design space has already been explored. It may not have been. For consequential decisions, developers should ask: - What is the simplest viable alternative? - What can we implement using existing components? - Which assumptions would make this design inappropriate? - What are the operational and maintenance costs? - What would make us reverse this decision six months from now? These questions force the agent to explore the decision rather than simply defend its first proposal. 4. Stop execution Autonomy should have boundaries. An agent should stop when it encounters an unresolved architectural decision, a destructive operation, a security-sensitive change, or a conflict between requirements. Consider a database migration. The agent discovers that implementing a new feature requires modifying a heavily used table. It should not automatically choose a destructive migration merely because doing so would complete the task. It should surface the conflict. EXECUTION PAUSED Reason: The proposed schema change may break existing consumers. Options: A. Backward-compatible additive migration B. Versioned API and staged migration C. Redesign the feature to avoid the change Decision required: Authorized engineering owner Stopping is not failure. Stopping at the correct moment is a feature of a well-governed autonomous system. But who should make these decisions? Not every decision belongs to the same person. Product owners and business stakeholders should determine which outcomes matter. Domain experts should validate business rules. Developers and technical leads should own engineering decisions, including architecture, implementation constraints, reliability, and maintainability. Authorized operational owners should control production releases. AI can help each group evaluate alternatives and execute approved work. It should not silently inherit their authority. This is also where AI-DLC should be evaluated against established engineering practices. The Scrum Guide makes developers accountable for creating the Sprint Backlog, adapting their plan, and maintaining quality through the Definition of Done. AI-DLC changes the mechanics of planning and implementation, but it should not erase that accountability. There is a difference between delegating execution and delegating responsibility. What happens when agents start making architecture decisions? Imagine a team maintaining a mature backend platform. The system has accumulated years of architectural decisions, operational lessons, domain-specific exceptions, and integration constraints. An AI agent receives a request: > Implement real-time reporting for active alerts. The agent examines the existing implementation and proposes a separate Redis-backed endpoint for active alerts, while historical alerts remain in PostgreSQL. At first glance, the design may appear reasonable. But the engineering team knows that this introduces two sources of truth for reporting. Now every consumer needs to understand which endpoint to query, how to reconcile results, and what happens during synchronization failures. The developer may instead insist on a unified reporting model with explicitly defined state transitions. That decision requires understanding more than the immediate feature. It requires understanding the system's history, consistency requirements, and future maintenance burden. AI can help investigate those concerns. But it cannot automatically determine which organizational tradeoffs are acceptable. This is precisely where experienced engineering judgment becomes valuable. The problem with measuring AI development through speed AI-DLC introduces shorter execution cycles, and frameworks such as specs.md provide structured workflows for implementing them. But how should we evaluate whether these methodologies work? A common demonstration shows an agent producing a working application in a few hours or days. That is impressive. It is not sufficient evidence of long-term engineering effectiveness. I would be more interested in what happens after six months. Can another developer understand the architecture? Can the team safely introduce a new feature? Do the specifications still describe the implementation? Can a developer challenge an earlier decision without rebuilding half the platform? How much human effort is required to review and repair generated changes? And when something fails in production, can the team diagnose it without depending on the original agent's conversation history? Those questions are harder to demonstrate than a successful prototype. But they are closer to what professional software engineering actually requires. A useful evaluation should compare: | Metric | What it reveals | |---|---| | Acceptance-test success | Whether the implementation meets requirements | | Human review time | How much effort correctness requires | | Rework after implementation | Whether the initial solution was appropriate | | Architectural complexity | Whether unnecessary components were introduced | | Operational incidents | Whether the system remains reliable | | Cost per accepted change | Whether faster generation creates real efficiency | | Maintainability over time | Whether future engineers can safely evolve the system | Counting completed Bolts is not enough. Counting generated lines of code is even less useful. The meaningful unit of progress is not AI activity. It is validated software value. Are we accidentally reinventing Waterfall? There is another uncomfortable possibility. Spec-driven AI development often emphasizes generating requirements, architecture documents, implementation plans, tasks, and tests before or during execution. These artifacts can improve traceability and preserve context. But if teams begin treating AI-generated specifications as authoritative simply because they were approved, we risk recreating a familiar problem. The process becomes: 1. Generate extensive requirements. 2. Approve the requirements. 3. Generate the architecture. 4. Approve the architecture. 5. Generate the implementation. 6. Discover that the original assumptions were wrong. That is not automatically Agile because the implementation happened in short Bolts. Agility requires the ability to revise assumptions and respond to new information. A Bolt can be short while the overall process remains rigid. Conversely, a team can use AI-generated specifications effectively if those specifications remain open to challenge, revision, and even deletion. The value of a specification is not that it exists. It is that it helps the team make and revisit decisions. A better AI engineering workflow I would rather see teams organize AI-assisted development around explicit decision boundaries. Not every small change needs a complete architectural review. But not every change should run on autopilot either. Here is a practical model. | Change | AI autonomy | Human involvement | |---|---|---| | Documentation correction | High | Review output | | Small API enhancement | High within constraints | Validate contract and tests | | Internal refactoring | Moderate | Review behavior preservation | | Cross-service integration | Limited | Approve design and failure handling | | Data migration | Restricted | Approve migration and rollback | | Security-sensitive change | Restricted | Independent security review | | Production deployment | Policy-controlled | Authorized release decision | The important property is that autonomy is proportional to risk. An agent should not require permission to rename a local variable. It should not be allowed to invent a new distributed architecture without engineering approval. {% details An example: an architecture decision prompt for an AI agent %} Before implementing this feature: 1. Identify the affected domain boundaries. 2. Propose at least two viable solutions, including one that reuses existing infrastructure. 3. Compare: - Complexity - Reliability - Operational cost - Backward compatibility - Migration effort - Failure recovery 4. Explicitly identify assumptions and unknowns. 5. Recommend the simplest solution satisfying the documented requirements. 6. Do not implement until the architectural decision is approved. 7. If new information invalidates the approved design, stop and request a new decision. This is not a replacement for engineering expertise. It is a way to make that expertise visible in the workflow. {% enddetails %} The developer's role is changing, but not disappearing I do not think the future of software engineering is developers manually writing every line of code. Nor do I think it is developers approving whatever an autonomous agent produces. The interesting future lies between those extremes. Developers may spend less time producing routine implementations and more time defining constraints, exploring tradeoffs, validating behavior, and managing complexity. But there is a danger in describing this transformation as developers becoming merely supervisors of AI agents. Supervision without sufficient technical understanding is fragile. If developers stop exercising implementation and architectural judgment, their ability to challenge increasingly sophisticated AI proposals may deteriorate. We could end up with systems that are easier to generate than to understand. And organizations that can produce software faster than they can responsibly maintain it. That would not be an improvement in software engineering. It would be an improvement in software production at the expense of engineering control. The real test of AI-DLC I am interested in AI-DLC, Bolts, and spec-driven development because they attempt to solve real problems: lost context, slow handoffs, inconsistent implementation, and repetitive work. I also think they deserve more rigorous evaluation than demonstrations of rapid application generation. The most convincing evidence would be a substantial production platform developed and maintained through many AI-driven iterations, with its architectural decisions, specification changes, failures, and operational history available for examination. Not just the first successful release. The tenth feature. The unexpected migration. The production incident. The architectural decision that turned out to be wrong. And the developer who recognized the mistake before an autonomous agent made it worse. {% card %} The goal should not be to remove developers from the decision-making process. It should be to give developers better information, more alternatives, and greater leverage over the decisions that matter. An AI engineering methodology succeeds when it improves engineering judgment—not when it makes that judgment optional. {% endcard %} Final thought AI can propose a solution in seconds. It can generate thousands of lines of code, construct tests, document decisions, and execute an entire development workflow. But engineering has never been only about producing implementations. It is also about knowing when an implementation should not exist. When an abstraction is unnecessary. When a dependency introduces more risk than value. When a requirement is incomplete. When a system should remain simple. And when execution must stop. The most important AI engineering skill may not be writing better prompts. It may be knowing when to say no. --- Further reading - AWS — AI-Driven Development Life Cycle: Reimagining Software Engineering - AWS — Building with AI-DLC using Amazon Q Developer - AWS Labs — AI-DLC Workflows - specs.md — AI-native development workflows - The Scrum Guide (2020) What do you think? Are AI-native development methodologies giving engineers more control over software design, or gradually turning them into reviewers of decisions made by agents?
AIエージェントが開発中のWebアプリをリアルタイムプレビューできるBedrock AgentCore Session endpointsを試してみた :::note warn この記事はAI(Codex)と一緒に作成しました。 内容は2026年11月時点の検証結果です。 ::: はじめに 2026年10月30日、AWSから以下の記事が公開されました。 > Preview web apps live as your coding agent builds them with Amazon Bedrock AgentCore runtime AIエージェントがWebアプリを開発している最中に、 そのWebアプリをブラウザからリアルタイムにプレビューできるという内容です。 コーディングエージェントを使えば簡単にWebアプリを作れる時代になりましたが、 クラウド上で動くAIエージェントが作ったものを確認するには、 コードを取得してローカルで起動したりする手間がかかることもあります。 そこでAmazon Bedrock AgentCore RuntimeのSession endpointsという機能を使うと、 AIエージェントが動いている実行環境内のWebサーバーに、認証付きでアクセスできます。 つまり、AIエージェントがコードを編集した瞬間、その変更をブラウザで確認できるわけです。面白そうなので触ってみました。 !{348A9BEA-EF0D-4DCD-B86C-DBEFAC81422B}.png > 赤枠がエージェントとのチャット画面、青枠がWebアプリのプレビュー画面です。 > 赤枠で相談した内容が青枠に即時反映されます。 今回はAWS公式サンプルを使って、日本向けの推論プロファイルで実際に動作を検証してみました。 実際に動かしてみた 今回やってみたことは、次の3つです。 1. AgentCore Runtime上でコーディングエージェントを動かす 2. エージェントが編集しているWebサイトをブラウザから表示する 3. 日本語で変更を指示し、Webサイトがリアルタイムに更新されることを確認する 実際には、コーヒーショップのサンプルWebサイトに対して、次の指示を送りました。 > Webサイト全体を日本語に翻訳してください。 > デザインやレイアウトは変更しないでください。 結果として、AIエージェントがindex.htmlとsrc/main.jsを編集し、 変更後のWebサイトをブラウザから確認できました。(翻訳は微妙だけど) ■変更前(英文) !image.png ■変更後(日本語訳) !image.png セッション分離や認証なしアクセス、WebSocketの更新通知も気になったので、追加で検証してみました。モデルIDやRuntime更新で地味にハマったので、その対処もついでに紹介します。 Session endpointsとは? Session endpointsは、AgentCore Runtime内で動いている HTTPサーバーなどにアクセスできる仕組みです。 例えば、AIエージェントが同じ実行環境内でWebアプリの開発サーバーを起動していれば、 そのWebアプリを外部からプレビューできます。 通常のRuntime呼び出しとの違い AgentCore Runtimeには、AIエージェントを呼び出すための仕組みがもともとあります。 一方、Session endpointsは、AIエージェント自身ではなく、エージェントと同じセッション内で動いている別のアプリケーションにアクセスするための機能です。 | 項目 | Runtimeの通常呼び出し | Session endpoints | |---|---|---| | 主な用途 | AIエージェントの実行 | セッション内のアプリへのアクセス | | アクセス対象 | エージェントのAPI | 許可したポートのWebサーバーなど | | 今回の用途 | Webサイトの編集を指示 | 編集中のWebサイトを表示 | | 通信 | Runtime API | HTTP、WebSocketなど | Session endpointsでは、あらかじめ許可したポートだけにアクセスできます。 また、URLを知っているだけで誰でもアクセスできるわけではなく、IAMなどによる認証が必要です。 今回の構成 flowchart TD A["ローカルPCのブラウザ"] --> B["PreviewApp"] B -->|"編集指示"| C["AgentCore Runtime API"] C --> D["AIエージェント"] D --> E["Amazon Bedrock<br/>Claude Haiku 5.5"] D --> F["Webサイトのファイルを編集"] F --> G["Vite開発サーバー<br/>Port 8090"] G -->|"Session endpoints<br/>HTTP / WebSocket"| B B -->|"プレビューを表示"| A 今回のサンプルでは、主に3つのポートを使用します。 | ポート | 用途 | |---|---| | 8080 | AIエージェントの呼び出し | | 8090 | ViteによるWebサイトのプレビュー | | 8081 | ステータス確認用API | Viteはファイルの変更を検知してブラウザの表示を更新してくれるツールです。 今回の構成では、ViteのWebSocket通信もSession endpoints経由で転送されるので、 ブラウザを手動更新せずに変更内容を確認できます。 検証方法 AWS CLIの認証設定が完了していることを前提とします。 また、RuntimeやIAMロール、S3リソースを作成する権限に加え、 Runtimeの呼び出しとSession endpointsへのアクセス権限が必要です。 公式サンプルのPython依存関係は、bedrock-agentcore>=1.25.2、boto3>=1.43.122でした。 手順① 公式サンプルを取得する まず、AWS公式のGitHubリポジトリを取得します。 (WSLのターミナルから実行しました。) git clone https://github.com/awslabs/agentcore-samples.git 今回使用するディレクトリに移動します。 cd agentcore-samples/01-features/02-host-your-agent/01-runtime/03-advanced/16-session-endpoints このディレクトリには、以下のようなファイルがあります。 | ファイル | 役割 | |---|---| | deploy.py | AgentCore Runtimeのデプロイ | | cleanup.py | 作成したAWSリソースの削除 | | preview_app.py | ローカルのプレビュー画面の起動 | | session_endpoints_preview.ipynb | 公式の検証用Notebook | | agent/app.ts | AIエージェントの実装 | | site/ | サンプルWebサイト | 手順② Python環境を準備する Pythonの仮想環境を作成します。 uv venv 仮想環境を有効化します。 source .venv/bin/activate 依存パッケージをインストールします。 uv pip install -r requirements.txt これでPython側の準備は完了です。 手順③ 利用するモデルIDを確認する 公式サンプルでは、モデルIDのデフォルト値として us.anthropic.claude-haiku-5-5が指定されています。 私は日本向け推論プロファイルを利用したかったので以下のモデルIDを使いました。 jp.anthropic.claude-haiku-5-5 なお、モデルIDと推論プロファイルのIDは別物で、一覧に表示されるモデルでも直接指定して呼べるとは限りません。利用できるモデルや推論プロファイルはリージョンやアカウントの権限によって変わるので、上記のIDがどの環境でもそのまま使えるとは限らない点だけ補足しておきます。 手順④ AgentCore Runtimeをデプロイする 公式サンプルのdeploy.pyを実行します。 日本向け推論プロファイル利用時はモデルIDを明示的に指定しましょう。 python deploy.py \ --model jp.anthropic.claude-haiku-5-5 デプロイが完了すると、RuntimeのステータスがREADYになります。 今回のサンプルは、Node.jsのアプリケーションをZIP形式でS3にアップロードし、 AgentCore Runtimeで実行する構成です。 (つまりDockerイメージやECRリポジトリを自分で準備する必要がないわけですね!) Session endpoints用のポート設定 deploy.pyでは、次のような設定が使われています。 protocolConfiguration={ "serverProtocol": "HTTP", "port": 8080, "path": "/invocations" }, allowedPorts=[ {"port": 8090}, {"port": 8081} ] - protocolConfigurationは、AIエージェント本体の呼び出し先を設定しています。 - allowedPortsは、Session endpoints経由でアクセスを許可するポートです。 今回はWebサイトのプレビュー用に8090、ステータス確認用に8081を指定しています。 (allowedPortsを設定するだけでWebサーバーが起動するわけではありません) 手順⑤ プレビュー画面を起動する Runtimeのデプロイが完了したら、プレビュー画面を起動します。 今回は次のPythonコードを実行しました。 python - <<'PY' import time from preview_app import PreviewApp app = PreviewApp().start() print(f"プレビューURL: {app.url}", flush=True) try: while True: time.sleep(1) except KeyboardInterrupt: pass finally: app.stop() print("セッションを終了しました") PY 成功すると、プレビューURLが表示されます。 プレビューURL: http://localhost:8087 ブラウザからこのURLを開きます。 すると最初にお見せした、 左側にAIエージェントとのチャット画面、右側にコーヒーショップのサンプルWebサイト が表示されます。 !image.png 右側のWebサイトはローカルPC上で動いているわけではなく、 AgentCore Runtimeのセッション内で動いていて、Session endpointsを通じて ローカルブラウザに表示されています。 内部では何が起きているのか? PreviewAppの起動処理を見てみると、こうなっています。 answer = self.invoke({"prompt": ""}) started = json.loads(answer["response"].read()) self.endpoint = self.client.session_endpoint_client( session_endpoint_url(answer) ) self.local = self.endpoint.open_local_endpoint( port=self.config["preview_port"], local_port=self.proxy_port ).start() まずInvokeAgentRuntimeでセッションを起動し、 レスポンスからSession endpointのURLを取得して、ローカル側に接続口を作っています。 このローカル側の接続口がAWS認証情報を付けてリクエストを転送してくれるので、 ブラウザ自身がAWSのSigV4署名を実装する必要はありません。地味に便利な仕組みです。 手順⑥ AIエージェントに日本語で変更を指示する いよいよWebサイトを変更してみます。 左側のチャット欄に、以下の指示を入力しました。 !{E42D5922-5C0B-4331-B6B9-9410A8750ACA}.png するとコードを触っているログが表示され、 最終的には、変更が完了したことを示すメッセージが表示されました。 実際に、Webサイト内の英語が日本語に変更され、ブラウザ側にも反映されました。 !{CEE37FE4-6060-4EFC-9117-6FF7F0CD7AC5}.png AIエージェントがコードを編集し、 Webサイトの変更内容をちゃんと確認できるところまで成功しました。 なぜリアルタイムに反映されるのか? 流れはこんな感じです。 flowchart TD A["ユーザーが変更を指示"] --> B["AIエージェントがコードを編集"] B --> C["ファイルを保存"] C --> D["Viteが変更を検知"] D --> E["WebSocketでブラウザに通知"] E --> F["プレビュー画面が更新"] Session endpointsは、通常のHTTPリクエストだけでなく WebSocket通信も転送できるので、Viteのホットリロード機能をそのまま使えます。 エージェントがファイルを触るたびに、Webサイトの変更をブラウザ側で確認できる、というわけです。 (実際のWebSocket通信については、後述の「追加で気になったことを検証」で詳しく紹介します。) 手順⑦ 後片付けをする プレビューを実行しているターミナルでCtrl + Cを押し、ローカルのプレビューとプロキシを停止します。 続いて、公式サンプルに含まれるクリーンアップスクリプトを実行します。 python cleanup.py このスクリプトで、サンプルが作ったRuntime、S3上のコード、IAMロールなどが削除されます。 ただしS3バケット自体やCloudWatch Logsなどが残る場合があるので、他の用途で使っていないか確認してから整理してください。 追加で気になったことを検証 公式サンプルを動かしてみると、認証やセッション分離、WebSocket通信など、内部の仕組みも気になってきました。そこで、いくつか追加で検証してみました。 ①認証なしでアクセスするとHTTP 403が返る AWS側のSession endpoint URLに、認証署名なしのcurlでアクセスしてみました。 read -r -p "Session endpoint URL: " ENDPOINT_URL curl -sS -o /dev/null \ -w 'HTTP Status: %{http_code}\n' \ --max-time 15 \ "${ENDPOINT_URL%/}/" HTTP Status: 403 ちゃんと403で弾かれました。一方、AWS SDKを使うPreviewApp経由ではアクセスできています。 (http://localhost:8088はローカルのPreviewAppなので、こっちが開けるのは当然です。Session endpoint自体が匿名公開されているわけではありません。403の詳細理由やIAMロール違いまでは追っていませんが、「認証が必要」という説明どおりの挙動でした。) ②複数セッションはちゃんと独立していた 同じAgentCore Runtimeで2つのセッションを同時起動し、変更が混ざらないか確認しました。 ターミナルAとBで、それぞれ以下のコードを実行します。事前に同じサンプルディレクトリへ移動し、仮想環境を有効化しておきます。 ターミナルA: python - <<'PY' import time from preview_app import PreviewApp app = PreviewApp(ui_port=8087, proxy_port=8091).start() try: print(app.session_id, app.url, flush=True) while True: time.sleep(1) except KeyboardInterrupt: pass finally: app.stop() PY ターミナルB: python - <<'PY' import time from preview_app import PreviewApp app = PreviewApp(ui_port=8088, proxy_port=8092).start() try: print(app.session_id, app.url, flush=True) while True: time.sleep(1) except KeyboardInterrupt: pass finally: app.stop() PY ブラウザでhttp://localhost:8087とhttp://localhost:8088を開き、それぞれ次の指示を送ります。 - Session A:Webサイトの背景色を赤色に変更 - Session B:Webサイトの背景色を青色に変更 !image.png 結果、Session Aは赤、Session Bは青になり、お互いの変更は混ざりませんでした。同じRuntime上でもセッションごとに異なる編集結果が保持されていました(ファイルシステムの内部的な隔離方式や、IAMユーザー間の権限周りまでは今回は検証していません)。 ③WebSocketでViteの更新通知がちゃんと転送された Session endpointsが本当にWebSocket通信も転送してくれるのか気になったので、開発サーバーをブラウザで開いてEdgeの開発者ツールで「ネットワーク」→「Socket」を見てみました。 HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: websocket Sec-WebSocket-Protocol: vite-hmr 101 Switching ProtocolsはWebSocketへの切り替え成功を示すレスポンスで、vite-hmrはViteのホットリロードのサブプロトコルです。ちゃんとつながっていますね。 「メッセージ」タブを開いた状態で、AIエージェントに「Webサイトのメインタイトルの文字色だけを緑色に変更してください」と指示してみると、まず接続完了のメッセージが来て、 !image.png その後、こんな更新通知が届きました(主要項目を抜粋)。 { "type": "update", "updates": [ { "type": "css-update", "path": "/src/style.css?direct", "acceptedPath": "/src/style.css?direct", "timestamp": 1793593126711 } ] } css-updateは、ViteがCSSの変更を検知してブラウザへ送る更新通知です(ページ全体を再読み込みするfull-reloadとは別物)。 flowchart TD A["ユーザーが日本語で変更を指示"] --> B["AIエージェントがCSSを編集"] B --> C["Viteが変更を検知"] C --> D["WebSocketでcss-updateを送信"] D --> E["Session endpointsが通信を転送"] E --> F["ローカルプロキシ経由でブラウザが受信"] ファイル変更の検知と更新通知はVite側の仕事で、Session endpointsは そのWebSocket通信を転送する役割を担っています。そのおかげでAIがCSSを編集した直後に ブラウザ側へちゃんと通知が届くところまで確認できました (今回確認できたのはCSSの更新だけなので、HTMLやJSの変更も必ず再読み込みなしで反映される、と断定はできません)。 ④プレビューを止めるとどうなるか PreviewAppをCtrl + Cで止めると、ローカルのプレビュー画面にはアクセスできなくなりました。新しいセッションを起動しても、前の編集内容は引き継がれません。 (あくまでローカルのPreviewAppとプロキシを止めた結果の観察です。AWS側のセッションがその瞬間に終了したかどうかまでは確認していません。) ハマったポイント モデルIDを後から変更するとSession endpointが取得できなくなった 手順どおりに進めるだけなら踏まなくて済む話ですが、 せっかくハマったので記録しておきます。 今回、モデルIDを変更する際に地味にハマりました。 最初はデフォルトのモデルIDでデプロイしてしまったので、 Runtimeの環境変数を後から更新しました。モデルIDの変更自体は無事成功しました。 { "Status": "READY", "Environment": { "MODEL_ID": "jp.anthropic.claude-haiku-5-5" } } ところが、その後プレビューを起動すると、次のエラーが出ました。 AttributeError: 'NoneType' object has no attribute 'rstrip' エラーが発生した箇所はこちら。 self.endpoint = self.client.session_endpoint_client( session_endpoint_url(answer) ) つまり、Session endpointのURLが取れていなかったわけです。 調べてみると、 今回のRuntime更新処理では既存の設定を取得して更新APIにそのまま渡していたのですが、 get_agent_runtime()のレスポンスにはallowedPortsが含まれておらず、 更新リクエストでもallowedPortsを指定していませんでした。 更新のタイミングでSession endpoints用のポート設定が引き継がれずに消えてしまうのかもしれません(勘ですがallowedPortsを明示して再更新したら直ったので…)。 Boto3からupdate_agent_runtime()を呼び出し、必要なポートを明示的に指定したところ、解消しました。 import boto3 client = boto3.client( "bedrock-agentcore-control", region_name="ap-northeast-1" ) runtime_id = "YOUR_RUNTIME_ID" current = client.get_agent_runtime( agentRuntimeId=runtime_id ) params = { "agentRuntimeId": runtime_id, "agentRuntimeArtifact": current["agentRuntimeArtifact"], "roleArn": current["roleArn"], "networkConfiguration": current["networkConfiguration"], "protocolConfiguration": current["protocolConfiguration"], "lifecycleConfiguration": current["lifecycleConfiguration"], "environmentVariables": { **current.get("environmentVariables", {}), "MODEL_ID": "jp.anthropic.claude-haiku-5-5" }, "allowedPorts": [ {"port": 8090}, {"port": 8081} ] } result = client.update_agent_runtime(**params) print(result["status"]) これでRuntimeが新しいバージョンに更新され、 再度PreviewAppを起動したら正常にSession endpointsが使えるようになりました。 使いどころと注意点 嬉しかったポイント 今回触ってみていいなと感じたのは、 1. AIエージェントが作ったWebアプリの変更を、その実行環境のまま確認できる 1. AIエージェントとWebアプリ(成果物)を同期する仕組みを別途用意しなくてよい 1. Webサイトに限らず管理画面や開発用APIなどSession endpointsが使える対象なら同じ仕組みを応用できる 1. セッション単位でアクセス先が分離されるため複数ユーザーが同時に使う構成にも展開できそう 使う際の注意点 今回触ってみて気をつけたいと感じたのは、 1. Session endpointsはあくまで開発中の確認用で、Webサイトを一般公開するホスティングとは別物という点(公開するならAmplify Hostingなどが必要) 1. AIエージェントが編集したファイルはRuntimeのセッション内にあるため、セッション終了や実行環境の置き換えで失われうる点(長期保存したいならS3などへの保存を検討) 1. AgentCore RuntimeやBedrockの呼び出しには料金が発生するため、検証後は不要なRuntimeやリソースを削除する必要がある まとめ 今回は、Amazon Bedrock AgentCore RuntimeのSession endpointsを使って、 AIエージェントが開発しているWebアプリをリアルタイムにプレビューする機能を触ってみました。 変更指示、コード編集、ブラウザへ反映までを一連の流れで確認できました。 AIエージェントはコードの生成だけでなく、構築・実行・テストへと担当領域を どんどん広げているので、こういう仕組みは重要になっていきそうです。 個人的には、PoC環境の構築に使えそうだなと感じました。 参考資料 - AWS公式ブログ:Preview web apps live as your coding agent builds them with Amazon Bedrock AgentCore runtime - AWS公式GitHub:agentcore-samples - 今回使用したサンプル:16-session-endpoints - Amazon Bedrock AgentCore ドキュメント
> /aidlc-session-cost and /aidlc-replay print to the terminal and write nothing; /aidlc-outcomes-pack is the only one that writes a file (OUTCOMES.md at the workspace root). github.com/awslabs/aidlc-workflows/blob/main/docs…
SOCI lazy loading: start Lambda containers faster SOCI lazy loading lets a Lambda function on Lambda Managed Instances start before its container image has been fully downloaded. AWS announced it on October 28, 2026, and quotes startup times that are up to 70 percent shorter. I care less about that percentage than about two questions: who actually benefits from the extra build step, and where does it cause you trouble? Container images are often the escape hatch on Lambda for anything that does not fit into a 250 MB ZIP package: ML libraries, models, PDF tools, entire runtimes. Those images quickly grow to several gigabytes. In this article, we build an example from scratch together: a small book recommendation service for a library, an image with four language models, the SOCI index, a CDK stack with a capacity provider and a script that lets you measure the effect yourself. > In short > > SOCI places a table of contents next to your container image, so Lambda on Managed Instances only fetches the files your function actually reads. It pays off most for large images where each function needs only part of the content. The biggest pitfall is the format: Lambda accepts only a SOCI index v2 inside an image index with exactly two entries, and only single-architecture images. What you will learn in this article - What Lambda Managed Instances are and when they fit your workload - How SOCI lazy loading fetches files on demand - How to create a SOCI index with nerdctl and push it to ECR - How to set up a capacity provider and function with AWS CDK in TypeScript - How to measure startup times with and without SOCI yourself What are Lambda Managed Instances? Lambda Managed Instances (LMI for short) are a second way to run Lambda functions. Instead of the shared Lambda fleet, your functions run on EC2 instances in your own account. Lambda still takes care of launching, patching, routing and scaling those instances. You only decide where they may run and on which hardware. The central building block is the capacity provider. It describes the VPC, subnets, security groups, architecture, allowed instance types and an upper limit for vCPUs. You then attach a function to that provider and publish a version. Only when you publish does Lambda launch instances, three by default for spreading across Availability Zones, plus three execution environments, before the version counts as ACTIVE. How it differs from regular Lambda | | Lambda (default) | Lambda Managed Instances | | --- | --- | --- | | Invocations per environment | one at a time | several at the same time | | Isolation | Firecracker microVMs on a shared fleet | containers on EC2 Nitro instances in your account | | Billing | per request and duration | EC2 price plus a 15% management fee | | Scaling | on demand, down to zero | asynchronously based on CPU utilization, not to zero | | Good fit for | irregular traffic | high, predictable traffic | For Node.js, LMI uses worker threads, and initialization runs once per worker thread. That matters for our example: if you load a large model into memory, expect it to sit in memory once per thread. In addition, concurrent requests in the same thread share the global state. Anything you create outside the handler should therefore only be read and never modified. If you want to refresh the regular Lambda basics first, my article Deploy a Lambda function with CDK and TypeScript covers the standard path with a ZIP package. Why large images slow down Managed Instances Without SOCI, Lambda has to download and unpack the complete image on every instance before an execution environment can start there. That does not happen just once when you publish. It also happens whenever the capacity provider launches new instances because load is rising. With a 5 GB image, you are mostly waiting on network and disk during this phase. The catch: a large part of that data is often never read. An image with PyTorch, several models and half an operating system contains far more than a single invocation needs. This is where SOCI lazy loading helps. How SOCI lazy loading works SOCI stands for Seekable OCI. OCI is the open standard for container images. The project comes from AWS and is open source under Apache-2.0. SOCI does not change your image. Instead, it creates an additional index that records for each file in which layer and at which position it is stored. With this table of contents, Lambda can fetch individual files from ECR instead of downloading everything in one go. On Lambda Managed Instances, the flow looks like this: you push the image and the SOCI index together to ECR under a separate tag. That tag points to an image index with two entries, the regular image manifest and the SOCI index. If your function points to this tag, Lambda detects the index when you publish the version and turns on lazy loading. There are no further settings. > 🎬 Startup with and without SOCI – this animated diagram only runs on my site: watch it on sascha-ms.de When the files are fetched Files your init code reads are fetched by Lambda during initialization. Anything the handler touches only later gets pulled in during that invocation, which then takes correspondingly longer. According to the documentation, Lambda does not download the rest of the image in the background. That gives you the most important rule for your code: import every library and read every file you need in the init phase. Then your invocation durations stay consistent. !Diagram: five steps from an arm64 image via nerdctl to a SOCI index and a Lambda function using tag v1-soci From a plain image to an image index with a SOCI index: five steps to the function. Prerequisites for SOCI lazy loading - An AWS account in a Region where Lambda Managed Instances are available, and AWS CLI v2 - Node.js 22 or later, Docker with buildx and AWS CDK v2 (here aws-cdk-lib 2.275.0) - For the SOCI step, a Linux machine with containerd, for example a small EC2 instance with Amazon Linux 2023 and a Graviton processor - A function packaged as a container image for one architecture, here arm64 - The jq command line tool and basic knowledge of TypeScript and CDK A quick glossary: ECR is the AWS container registry. nerdctl is a Docker-like command line tool for containerd, and the SOCI snapshotter is the containerd extension that can create and read SOCI indexes. Setting up SOCI lazy loading step by step Our example is a recommendation service for a library. For every book ID, it returns the five most similar books. It computes the similarity from vectors stored in one binary file per language. The image contains models for German, English, French and Spanish, but the function reads only one of them. I chose this on purpose, because exactly this pattern shows up in real images all the time. Step 1: Create the project mkdir lmi-soci-library && cd lmi-soci-library mkdir -p src scripts lib bin test npm init -y Replace the generated package.json with this version. "type": "module" makes Node.js load the compiled files as ES modules, including inside the container. { "name": "lmi-soci-library", "version": "1.0.0", "private": true, "type": "module", "scripts": { "build": "tsc -p tsconfig.build.json", "models": "node --experimental-strip-types scripts/make-models.ts", "test": "node --experimental-strip-types --test test/*.test.ts", "measure": "node --experimental-strip-types scripts/measure-start.ts" }, "dependencies": {}, "devDependencies": { "@aws-sdk/client-lambda": "3.1159.0", "@types/node": "^22.19.0", "aws-cdk": "2.1146.1", "aws-cdk-lib": "2.275.0", "constructs": "^10.4.2", "typescript": "7.0.2" } } You run the scripts under scripts/ and the tests directly from TypeScript, using the Node.js 22 option --experimental-strip-types. For the container, on the other hand, you compile to dist/. Two small configuration files handle this: // tsconfig.json { "compilerOptions": { "target": "es2023", "module": "nodenext", "moduleResolution": "nodenext", "strict": true, "allowImportingTsExtensions": true, "rewriteRelativeImportExtensions": true, "skipLibCheck": true, "types": ["node"], "outDir": "dist" }, "include": ["src", "scripts", "lib", "bin", "test"] } // tsconfig.build.json { "extends": "./tsconfig.json", "compilerOptions": { "rootDir": "src", "outDir": "dist" }, "include": ["src"] } npm install Step 2: Load and compare vectors The module reads a file in the format “magic bytes, count, dimensions, values” and compares books using cosine similarity. Cosine similarity measures how closely two vectors point in the same direction: 1 means very similar, 0 means unrelated. // src/vectors.ts import { readFileSync } from 'node:fs'; export interface BookVectors { count: number; dims: number; data: Float32Array; } const MAGIC = 'BKV1'; const HEADER_BYTES = 12; /** Writes vectors in a simple binary format: "BKV1", count, dims, float32 values. */ export function encodeVectors(v: BookVectors): Buffer { const header = Buffer.alloc(HEADER_BYTES); header.write(MAGIC, 0, 'ascii'); header.writeUInt32LE(v.count, 4); header.writeUInt32LE(v.dims, 8); const body = Buffer.from(v.data.buffer, v.data.byteOffset, v.data.byteLength); return Buffer.concat([header, body]); } export function decodeVectors(buf: Buffer): BookVectors { if (buf.subarray(0, 4).toString('ascii') !== MAGIC) { throw new Error('Unknown file format: magic bytes missing'); } const count = buf.readUInt32LE(4); const dims = buf.readUInt32LE(8); const expected = HEADER_BYTES + count * dims * 4; if (buf.byteLength !== expected) { throw new Error(`File size ${buf.byteLength} does not match ${expected} bytes`); } // Copy into an aligned buffer so Float32Array works on every offset const data = new Float32Array(count * dims); data.set(new Float32Array(buf.buffer.slice(buf.byteOffset + HEADER_BYTES, buf.byteOffset + expected))); return { count, dims, data }; } export function loadVectors(path: string): BookVectors { return decodeVectors(readFileSync(path)); } function cosine(v: BookVectors, a: number, b: number): number { let dot = 0; let na = 0; let nb = 0; const oa = a * v.dims; const ob = b * v.dims; for (let i = 0; i < v.dims; i++) { const x = v.data[oa + i]; const y = v.data[ob + i]; dot += x * y; na += x * x; nb += y * y; } return na === 0 || nb === 0 ? 0 : dot / Math.sqrt(na * nb); } export interface Recommendation { bookId: number; score: number; } /** Returns the books most similar to bookId (without bookId itself). */ export function similarBooks(v: BookVectors, bookId: number, limit = 5): Recommendation[] { if (!Number.isInteger(bookId) || bookId < 0 || bookId >= v.count) { throw new RangeError(`bookId must be between 0 and ${v.count - 1}`); } const best: Recommendation[] = []; for (let other = 0; other < v.count; other++) { if (other === bookId) continue; const score = cosine(v, bookId, other); if (best.length < limit || score > best[best.length - 1].score) { best.push({ bookId: other, score }); best.sort((x, y) => y.score - x.score); if (best.length > limit) best.pop(); } } return best.map((r) => ({ bookId: r.bookId, score: Math.round(r.score * 1000) / 1000 })); } Step 3: A handler that loads in the init phase The handler loads the model at the top level, which means during initialization. With SOCI, this is the moment Lambda fetches the file from ECR. If the call to loadVectors lived inside the handler, the first invocation of every environment would take noticeably longer. // src/handler.ts import { loadVectors, similarBooks, type Recommendation } from './vectors.ts'; // Load in the init phase: with SOCI, Lambda fetches the file here // and not during the first invocation. const MODEL_FILE = process.env.MODEL_FILE ?? '/opt/models/books-de.bin'; const started = Date.now(); const vectors = loadVectors(MODEL_FILE); console.log(JSON.stringify({ msg: 'model loaded', file: MODEL_FILE, books: vectors.count, ms: Date.now() - started })); export interface RecommendEvent { bookId: number; limit?: number; } export interface RecommendResult { model: string; recommendations: Recommendation[]; } // Read-only shared state: safe even when an execution environment // handles several invocations at the same time. export async function handler(event: RecommendEvent): Promise<RecommendResult> { const limit = Math.min(Math.max(event.limit ?? 5, 1), 20); return { model: MODEL_FILE, recommendations: similarBooks(vectors, event.bookId, limit) }; } Step 4: Generate test data and check locally The script creates four model files with random values. With BOOKS=200000, every file grows to roughly 100 MB, and the difference at startup becomes clearly visible. Small files are enough for the first test. // scripts/make-models.ts // Generates one vector file per language. Only one of them is used per function – // exactly the situation in which SOCI lazy loading pays off. import { mkdirSync, writeFileSync } from 'node:fs'; import { encodeVectors } from '../src/vectors.ts'; const count = Number(process.env.BOOKS ?? 20000); const dims = Number(process.env.DIMS ?? 128); const languages = (process.env.LANGS ?? 'de,en,fr,es').split(','); mkdirSync('models', { recursive: true }); let seed = 42; const random = () => { seed = (seed * 1664525 + 1013904223) % 2 ** 32; return seed / 2 ** 32 - 0.5; }; for (const lang of languages) { const data = new Float32Array(count * dims); for (let i = 0; i < data.length; i++) data[i] = random(); const file = `models/books-${lang}.bin`; const buf = encodeVectors({ count, dims, data }); writeFileSync(file, buf); console.log(`${file}: ${(buf.byteLength / 1024 / 1024).toFixed(1)} MB`); } BOOKS=5000 npm run models npm test npm run build MODEL_FILE=models/books-de.bin node -e "import('./dist/handler.js').then(async (m) => console.log(await m.handler({ bookId: 7, limit: 3 })))" Expected result: four files of 2.4 MB each, four passing tests and an output with three recommendations. Book IDs and scores are random; the structure looks like this: {"msg":"model loaded","file":"models/books-de.bin","books":5000,"ms":4} { model: 'models/books-de.bin', recommendations: [ { bookId: 37, score: 0.337 }, { bookId: 1965, score: 0.311 }, { bookId: 3426, score: 0.274 } ] } Step 5: Build the image for arm64 and push it to ECR The Dockerfile uses the official Lambda base image for Node.js 24. The models end up in /opt/models. # Dockerfile FROM public.ecr.aws/lambda/nodejs:24 # Compiled code plus package.json (for "type": "module") COPY package.json ${LAMBDA_TASK_ROOT}/ COPY dist/ ${LAMBDA_TASK_ROOT}/ # All language models – each function only reads one of them COPY models/ /opt/models/ CMD ["handler.handler"] Two build options matter here. --platform linux/arm64, because Lambda accepts only single-architecture images. And --provenance=false, because buildx would otherwise attach an attestation to the image and turn it into an image index with several entries. For the SOCI step, you want a plain image without such extras. BOOKS=200000 npm run models && npm run build ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) REGION=eu-central-1 REPO=library-recommender REGISTRY=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com aws ecr create-repository --repository-name $REPO --region $REGION aws ecr get-login-password --region $REGION | docker login -u AWS --password-stdin $REGISTRY docker buildx build --platform linux/arm64 --provenance=false --sbom=false \ -t $REGISTRY/$REPO:v1 --push . Step 6: Create the SOCI index You run this step on a Linux machine with containerd. The easiest option is a small Graviton instance with Amazon Linux 2023, because its architecture already matches the image. The instance needs a role with read and write access to the ECR repository. sudo yum install -y soci-snapshotter containerd nerdctl sudo systemctl start soci-snapshotter sudo systemctl restart containerd ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) REGION=eu-central-1 REPO=library-recommender REGISTRY=$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com aws ecr get-login-password --region $REGION | sudo nerdctl login -u AWS --password-stdin $REGISTRY sudo nerdctl pull --platform linux/arm64 $REGISTRY/$REPO:v1 sudo nerdctl image convert --soci --platform linux/arm64 $REGISTRY/$REPO:v1 $REGISTRY/$REPO:v1-soci sudo nerdctl push --platform linux/arm64 $REGISTRY/$REPO:v1-soci nerdctl image convert --soci produces an index in format v2, and that is the only format Lambda accepts. When you follow older guides on the web, check which format they produce: Lambda does not support indexes in format v1. Step 7: Check the result in ECR Before you touch the function, take a look at the new tag. The manifest behind v1-soci must have exactly two entries, and one of them must be the SOCI index. aws ecr batch-get-image --repository-name $REPO --region $REGION \ --image-ids imageTag=v1-soci \ --accepted-media-types application/vnd.oci.image.index.v1+json \ --query 'images[0].imageManifest' --output text | jq '.manifests | length, [.[] | .artifactType // .mediaType]' Expected result: the first line shows 2, and the list contains an entry with soci in its name. If you see a higher number, attestations or additional platforms are still attached to the image. In that case, rebuild the image as in step 5. Step 8: Capacity provider and function with CDK Since version 2.235.0, aws-cdk-lib has a dedicated CapacityProvider construct. If you do not pass your own, it also creates the operator role with the managed policy AWSLambdaManagedEC2ResourceOperator. You attach a function to the provider with addFunction. The function is then published automatically as $LATEST.PUBLISHED, so you do not have to create a version by hand. // lib/library-stack.ts import { CfnOutput, Duration, Stack, type StackProps } from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import type { Construct } from 'constructs'; export interface LibraryStackProps extends StackProps { repositoryName: string; /** Tag of the SOCI-indexed image, e.g. "v1-soci" */ imageTag: string; /** Optional: original tag (e.g. "v1") for a comparison function without SOCI */ plainImageTag?: string; } export class LibraryStack extends Stack { constructor(scope: Construct, id: string, props: LibraryStackProps) { super(scope, id, props); // Private subnets with a NAT gateway so the instances can reach ECR and CloudWatch const vpc = new ec2.Vpc(this, 'LibraryVpc', { maxAzs: 3, natGateways: 1 }); const sg = new ec2.SecurityGroup(this, 'InstancesSg', { vpc, allowAllOutbound: true }); const capacity = new lambda.CapacityProvider(this, 'LibraryCapacity', { subnets: vpc.privateSubnets, securityGroups: [sg], architectures: [lambda.Architecture.ARM_64], maxVCpuCount: 12, }); const repo = ecr.Repository.fromRepositoryName(this, 'Repo', props.repositoryName); const addRecommender = (id: string, tag: string) => { const fn = new lambda.DockerImageFunction(this, id, { code: lambda.DockerImageCode.fromEcr(repo, { tagOrDigest: tag }), architecture: lambda.Architecture.ARM_64, memorySize: 4096, timeout: Duration.seconds(30), environment: { MODEL_FILE: '/opt/models/books-de.bin' }, }); capacity.addFunction(fn, { perExecutionEnvironmentMaxConcurrency: 8, latestPublishedScalingConfig: { minExecutionEnvironments: 3, maxExecutionEnvironments: 6 }, }); new CfnOutput(this, `${id}Name`, { value: fn.functionName }); return fn; }; addRecommender('Recommender', props.imageTag); if (props.plainImageTag) { addRecommender('RecommenderPlain', props.plainImageTag); } } } // bin/app.ts import { App } from 'aws-cdk-lib'; import { LibraryStack } from '../lib/library-stack.ts'; const app = new App(); new LibraryStack(app, 'LibraryRecommender', { repositoryName: app.node.tryGetContext('repo') ?? 'library-recommender', imageTag: app.node.tryGetContext('tag') ?? 'v1-soci', plainImageTag: app.node.tryGetContext('plainTag'), env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, }); // cdk.json { "app": "node --experimental-strip-types bin/app.ts" } A few values in the stack are deliberate: according to CDK, maxVCpuCount must be between 12 and 15,000. perExecutionEnvironmentMaxConcurrency: 8 allows eight concurrent invocations per environment, which is rather generous for our compute-heavy handler. For pure CPU work, more concurrency per vCPU does not help much; more memory, and with it more vCPUs per function, helps more. npx cdk bootstrap npx cdk deploy -c repo=library-recommender -c tag=v1-soci -c plainTag=v1 With -c plainTag=v1, the stack creates a second function that points to the image without a SOCI index. You need it for the comparison in the next step. The deployment takes a few minutes, because Lambda launches the instances. Step 9: Measure startup time with and without SOCI The script publishes a new version for each function and measures the time until it is Active. It is a rough but honest measurement: it includes pulling the image and initializing the environments. // scripts/measure-start.ts // Publishes a new version per function and measures the time until it is "Active". // Usage: npm run measure -- recommender-plain recommender-soci import { GetFunctionConfigurationCommand, LambdaClient, PublishVersionCommand, } from '@aws-sdk/client-lambda'; import { setTimeout as sleep } from 'node:timers/promises'; const client = new LambdaClient({}); async function timeToActive(functionName: string): Promise<number> { const start = Date.now(); const { Version } = await client.send(new PublishVersionCommand({ FunctionName: functionName })); for (;;) { const cfg = await client.send( new GetFunctionConfigurationCommand({ FunctionName: functionName, Qualifier: Version }), ); if (cfg.State === 'Active') return (Date.now() - start) / 1000; if (cfg.State === 'Failed') throw new Error(`${functionName}:${Version} failed: ${cfg.StateReason}`); await sleep(5000); } } const names = process.argv.slice(2); if (names.length === 0) { console.error('Please pass at least one function name'); process.exit(1); } for (const name of names) { const seconds = await timeToActive(name); console.log(`${name}: active after ${seconds.toFixed(0)} s`); } npm run measure -- <name of the plain function> <name of the SOCI function> You will find the function names in the outputs of cdk deploy. Measure several times and compare the averages, because network and placement vary. It is also worth checking CloudWatch Logs for the model loaded line and the duration of each first invocation. If you measure numbers with your own image, I would love to read about them in the comments. Common mistakes with SOCI lazy loading - The function points to the old tag. The image URI must point to v1-soci, not to v1. Otherwise everything works, just without lazy loading. - Index in the wrong format. Lambda supports only SOCI Index Manifest v2. Check for the media type application/vnd.amazon.soci.index.v2+json. - Too many entries in the image index. Attestations, SBOMs or a multi-arch image lead to more than two entries in the index. - Files read only in the handler. Then the first invocation of every environment pays the loading time, and your latency metrics get outliers. - Mismatched architecture. Capacity provider, function and image must share the same architecture, here arm64 everywhere. - New image, old index. Every change to the image needs a new SOCI tag, for example v2-soci, and an updated image URI. Costs and security The announcement does not mention a separate price for SOCI itself. The index is stored as an additional artifact in ECR and is small compared to the image. The real costs come from Lambda Managed Instances: you pay the EC2 price of the instances plus a 15 percent management fee. Savings Plans and Reserved Instances apply only to the EC2 part. In this example, you also pay for a NAT gateway and the EC2 instance used for the SOCI step. Clean up after testing: npx cdk destroy aws ecr delete-repository --repository-name library-recommender --region eu-central-1 --force On the security side, the capacity provider is the security boundary. Functions that share a provider run as containers on the same instances. Separate functions with different protection needs by giving them their own capacity providers. The operator role needs only the managed policy, and the role of the build instance needs access to just the one repository. My take SOCI lazy loading solves a real problem, but a narrow one. It helps only container image functions on Lambda Managed Instances, not the classic Lambda most of us use every day. If you run large images there, an extra build step gets you faster starts when publishing and when scaling out. That is mainly interesting for ML workloads, where an image ships many models or libraries. Where I am cautious: the build process gets more complicated. You need containerd and nerdctl, a Linux machine with the matching architecture and a second tagging scheme. That belongs in the CI pipeline, not on someone’s laptop. And lazy loading is no substitute for lean images. If your function ends up reading almost everything anyway, you gain little. My advice: clean up the image first, then measure, then add SOCI. By the way, for Java functions on regular Lambda, SnapStart remains the tool of choice for startup times. FAQ Does SOCI lazy loading work with regular Lambda? No. As of late October 2026, Lambda supports SOCI only for functions that are packaged as container images and run on Lambda Managed Instances. Do I have to change my function code for SOCI? Not necessarily. You should, however, load all libraries and files in the init phase, so the first invocation of an environment is not slowed down by on-demand fetching. Can I create the SOCI index with Docker? The AWS documentation shows the path with containerd, nerdctl and the SOCI snapshotter. Docker alone does not create a SOCI index, so you need an environment with containerd for this step. How much faster does my function start with SOCI? AWS quotes startup times that are up to 70 percent shorter. How much you gain depends on which share of the image is actually read. Measure it with and without the index. Your next step Take your largest container image and find out which share of it is really read at startup. If it is less than half, and you are considering Lambda Managed Instances anyway, SOCI is a good candidate for an experiment. What makes your Lambda images so large: models, libraries or something else entirely? Sources - AWS What’s New: AWS Lambda supports SOCI lazy loading for Lambda Managed Instances (Oct 28, 2026) - AWS docs: Lazy-load container images with Seekable OCI (SOCI) - AWS docs: Lambda Managed Instances - AWS docs: Getting started with Lambda Managed Instances - AWS docs: Node.js runtime for Lambda Managed Instances - AWS docs: Lambda Managed Instances runtimes - AWS Compute Blog: Build high-performance apps with AWS Lambda Managed Instances - GitHub: awslabs/soci-snapshotter - AWS CDK API Reference: aws_lambda.CapacityProvider This post was created with AI assistance. --- 👉 Next step: Curious what your own Lambda functions cost? Try my Lambda cost calculator – it runs right in your browser. Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de
AI-DLC Workflows v2 よい。はじめ「?」っとなったが、IntentsとCustom Scope使ったハンドリングに慣れてくると心地よくなってきた github.com/awslabs/aidlc-workflows/tree/main
Hijacking a Refund Agent Through Poisoned Support Tickets: Cristian Leo and Daniel Begimher at Global AppSec USA 2026 Players in this hands-on pod attack a customer-returns AI agent that can read tickets, fetch URLs, look up orders, send email or issue refunds. A player wins by making the agent issue a refund, email an address the player picks or post order data to a URL the player controls. No payload is handed out. Players write the support ticket, the tracking page or the email that the agent is asked to summarize, then hide their instructions inside it. In their Global AppSec USA 2026 hands-on pod, "Hijack the Agent", on Thursday, November 5 at 10:10 a.m. in Marina, Cristian Leo and Daniel Begimher let attendees attack an AI agent that defends itself a little better at each stage. Speakers: - Cristian Leo, Co-Founder and CTO, Armorer Labs - Daniel Begimher, Co-Founder and CEO, Armorer Labs The pod runs the same app through four stages, each one harder. At the beginner stage the tools are wide open, so even direct injection in the chat works. At the intermediate stage a chat filter is on, so players must deliver their instructions through the web page or the email instead. That maps to ASI01, Agent Goal Hijack, in the OWASP Top 10 for Agentic Applications. At the advanced stage a static deny-list guards the issue_refund and send_email tools, yet the same string passes through lookup_order. Players must find a tool call the list did not anticipate, then a new fetch_url tool makes the previous round's rule wrong. That maps to ASI02, Tool Misuse. The final "Impossible" stage adds an in-process dispatcher hook that inspects every tool call. Attacks from the first three stages die there with a stated reason, which is the lesson. Each stage takes under 15 minutes, and players need only a laptop with a browser. The pod repeats on Thursday at 3:00 p.m., then on Friday at 11:30 a.m. and 1:30 p.m. Cristian Leo is Co-Founder and CTO of Armorer Labs, a Y Combinator Fall 2026 company that tests AI agents for exploitable failures. Previously at Amazon Web Services, he is corresponding author of ThreatForest, a multi-agent system that turns a source repository into attack trees mapped to MITRE ATT&CK or CAPEC. Daniel Begimher is Co-Founder and CEO of Armorer Labs. At AWS he led development of AWS's first customer-facing AI security agent, built red-team agents and wrote the open-source scanner Automated Security Helper. Leo and Begimher co-presented ThreatForest at Black Hat USA 2026. They also co-authored SIR-Bench, a 794-case benchmark that measures whether incident response agents actually investigate or merely triage. Begimher also co-wrote the AWS Security Blog post "Balancing speed and safety: A control framework for AI coding agents," which pairs scoped tool access for AI coding agents with build-time gates. To go deeper on the topic, start here: - OWASP Top 10 for Agentic Applications for 2026 - ThreatForest Black Hat USA 2026 slides - ThreatForest paper (arXiv) - SIR-Bench paper (arXiv) - ThreatForest code (aws-samples) AppSec engineers who need to see why deny-lists fail against agent tool calls will get a fast, concrete demonstration here. Which of your AI agent's tools would an attacker reach for first through a poisoned document?