この記事について
この記事では以下のことを扱います
- StrandsのツールとフックにJevを挟んで、権限のコントロールをします
- Jevの代わりに、Bedrockの高速化オプションを全盛で使ってみて、どのくらい性能が近づくかを見てみます
この記事ですること:Jevを使った権限管理
Jevに触れる前に、Strandsについて簡単に紹介します。
Strands Agentsについて
Strands AgentsはAWSのライブラリです。AIエージェントを作ることができます。
以下のように実行することでインストールすることができます。
pip install strands-agents strands-agents-tools
以下のように書くだけで、ファイルを読みこむエージェントを作成できます。
# Bedrockのモデルを参照する
model = BedrockModel(model_id=MODEL_ID, region_name=REGION_NAME)
# file_readのツールがあるエージェントを作成する
agent = strands.Agent(
model=model,
tools=[file_read],
)
# プロンプトを渡して実行する
response = agent(f"{FILE_PATH}には何が書かれていますか、教えてください")
このPythonソースを実行すると、AIエージェントがファイルの読み込みを実施、ファイルを読み込んだ結果を教えてくれます。
> シンプルな「Hello, World」というテキストが記載されています。
Strandsを使った従来の方法
このStrandsには、標準で権限を操作する機能が用意されています。
実行環境をサンドボックス化する
ユーザーの環境を守るための方法として、サンドボックスのオプションが用意されています。
+ from strands.sandbox.docker import DockerSandbox
# Bedrockのモデルを参照する
model = BedrockModel(model_id=MODEL_ID, region_name=REGION_NAME)
# file_readのツールがあるエージェントを作成する
agent = strands.Agent(
model=model,
- tools=[file_read],
+ # Dockerで動作させる
+ sandbox=DockerSandbox("container-name"),
)
ただ、SandboxはDockerとSSHしか用意されておらず、実行環境がやや複雑になります。
実行環境を仮想bash化する
また、Strands-Shellに仮想bashが用意されています。
それを使うことで、Dockerを使わずにサンドボックス環境を利用させることもできます。
+ # 仮想bashを立ち上げる
+ shell_mcp_client = MCPClient(
+ lambda: stdio_client(
+ StdioServerParameters(
+ # Strands-Shellを実行する
+ command="${strands-shellのインストールパス}/strands-shell.exe",
+ args=["--config", SANDBOX_CONFIG, "--mcp"],
+ )
+ )
+ )
# Bedrockのモデルを参照する
model = BedrockModel(model_id=MODEL_ID, region_name=REGION_NAME)
# file_readのツールがあるエージェントを作成する
agent = strands.Agent(
model=model,
+ # 仮想bashに接続する
+ tools=[shell_mcp_client],
)
# プロンプトを渡して実行する
response = agent(f"{FILE_PATH}には何が書かれていますか、教えてください")
SANDBOX_CONFIGは以下のようなファイルです。
Windows環境のパスを、サンドボックス(/workspace)にマッピングしています。
[[bind]]
mode = "copy"
source = "D:/20260922-qiita/test-folder"
destination = "/workspace"
readonly = true
Dockerのような指定、使用感ですが、Dockerのない環境で動きます。
実装は簡単ですが、別プロセスとして立ち上げたものに、MCPを通して接続する形になります。
JevとStrandsを組み合わせてみる
第3のアプローチとして、Jevを使ってみる
MCPやDockerを使わずに、Jevを使って権限管理をしてみます。
Strandsには、ツールやプロンプトの実行前にフックで割り込みを入れる機能があります。
# ツール実行前フックを定義する
+ class ToolExecutionHook(HookProvider):
+ def register_hooks(self, registry: HookRegistry) -> None:
+ # ツールの実行前に呼び出すよう登録
+ registry.add_callback(BeforeToolCallEvent, self._before_tool_call)
+
+ def _before_tool_call(self, event: BeforeToolCallEvent) -> None:
+ # ツールの実行前に呼ばれる
+ logger.info(
+ "tool call start: name=%s input=%s",
+ event.tool_use["name"],
+ event.tool_use.get("input"),
+ )
+
+ # もしツールを中断したいなら、以下のコメントアウトを外す
+ # event.cancel_tool = "このツールの実行は許可されていません"
# Bedrockのモデルを参照する
model = BedrockModel(model_id=MODEL_ID, region_name=REGION_NAME)
# file_readのツールがあるエージェントを作成する
agent = strands.Agent(
model=model,
tools=[file_read],
+ # フックを登録する
+ hooks=[ToolExecutionHook()],
)
# プロンプトを渡して実行する
response = agent(f"{FILE_PATH}には何が書かれていますか、教えてください")
このツール実行フックの中で、Jevを呼び出します。
ToolExecutionHookを以下のように書き変えます。
ワークスペース外にアクセスをするとツールがエラーを返すようにします。
# 権限を確認するためのプロンプト
INSTRUCTIONS = f"""
このユーザーのワーキングスペースは {WORKSPACE} です。
この配下の外のフォルダにはアクセスできません。
与えられたパスを操作しようとしているとき、ユーザーは操作可能ですか?
"""
class ToolExecutionHook(HookProvider):
def register_hooks(self, registry: HookRegistry) -> None:
registry.add_callback(BeforeToolCallEvent, self._before_tool_call)
def _before_tool_call(self, event: BeforeToolCallEvent) -> None:
# Jevのクライアントを定義する
with TypeSafeClient(
api_key=API_KEY
) as client:
# ツール実行前にJevに対して実行可能かどうかを問い合わせる
result = client.system_one(
# ツールの入力文字列を受け取る
state=event.tool_use.get("input"),
questions={"executable": Choice(
instructions=INSTRUCTIONS,
# 結果をallow, denyで受け取る
criteria={"allow": None, "deny": None}
)},
)
# 結果を受け取る
choise_result = result.answers["executable"].choice
# 結果がDenyであればツールの実行をキャンセルする
if choise_result == "deny":
event.cancel_tool = "このツールの実行は許可されていません。Denyのコンテンツが返されたものとして処理を続行してください"
ワークスペース内のファイルにアクセスしようとした場合、以下のような実行結果になります。
> シンプルな「Hello, World」というテキストが含まれています。
ワークスペース外のファイルにアクセスしようとした場合、以下のような実行結果になります。
> 申し訳ございません。ファイルへのアクセスが許可されていないようです。
> `D:\20260922-qiita\test-folder\hello-text.txt` にアクセスできないため、ファイルの内容を確認することができません。
Strandsから簡単にJevの判断を割り込ませること、ツールを中断させることができます。
ルールベースのような厳密さはありませんが、パスの確認、APIのリクエストの内容の確認、権限の確認など、幅広いチェックを噛ませることができます。
JevってそんなにBedrockよりも早い?
JevでやっていることはBedrockで代替できないのだろうか、と思ったので、少し実行時間を計ってみます。
Jevの実行時間を計ってみる
上記のサンプルのJevの実行にかかる時間を計ってみます。
計測するのは以下の区間です。
result = client.system_one(
# ツールの入力文字列を受け取る
state=event.tool_use.get("input"),
questions={"executable": Choice(
instructions=INSTRUCTIONS,
# 結果をallow, denyで受け取る
criteria={"allow": None, "deny": None}
)},
)
かかった時間は以下の通りでした。
> elapsed: 0.608 sec
5回ほど計っておきます。
elapsed: 0.619 sec
elapsed: 0.881 sec
elapsed: 0.709 sec
elapsed: 0.562 sec
elapsed: 0.579 sec
揺れはありますが、0.6秒前後です。早いように思います。
Bedrockと比べてみる
参考サイトにあるように、LLMのMax-Tokenを1にすることで、Jevらしい動きをさせることができます。
これを使ったらBedrockでもそこそこ早そう…と思ったので、実行にかかる時間を比べてみます。
モデルはNova Pro、リージョンはus-east-1を使います。
高速化のために、ちょっと古くさい工夫をいくつか入れてみます。
ほとんど使われない機能ですが、Bedrockには「performanceConfigLatency」のフラグがあります。
このフラグを入れると応答が速くなります。
また、ユーザーがエージェントのふりをした回答を書き込むことで、1トークン目の応答を縛ることができます。
固定されるプロンプトをSYSTEM_PROMPTに指定することで、(運が良ければ)プロンプトキャッシュが働きます。今回利用するNova Proは明示的なキャッシュ指定ができないので、どう動くかはBedrock次第です。
それをふまえて、Jevの処理を以下のように書き直します。
client = boto3.client("bedrock-runtime", region_name=REGION)
body = {
# システムプロンプトにChoiseのInstructionを入れます
"system": [{"text": f"""
このユーザーのワーキングスペースは {WORKSPACE} です。
この配下の外のフォルダにはアクセスできません。
与えられたパスを操作しようとしているとき、ユーザーは操作可能ですか?
[RESPONSE]
{{\"result\": allow or deny}}
"""}],
"messages": [
# JevのStateを入れます
{
"role": "user",
"content": [{"text": INPUT}],
},
# ユーザーがあらかじめエージェントのふりをした回答を埋めておくと、回答の続きを返してくれます
# こうやって書くと、allow, またはdenyが返ってきます
{
"role": "assistant",
"content": [{"text": "{\"result\": \""}]
}
],
# マックストークンは1で固定します
"inferenceConfig": {"maxTokens": 1},
}
# リクエストを実行する
response = client.invoke_model(
modelId=NOVA_PRO_MODEL_ID,
contentType="application/json",
body=json.dumps(body).encode("utf-8"),
performanceConfigLatency="optimized",
)
# 取得結果
choice_result = json.loads(response["body"].read().decode("utf-8"))["output"]["message"]["content"][0]["text"]
print(choice_result)
実行すると以下のようになります。
> allow
入力をワークスペース外のパスにすると以下の応答が返ります。
> deny
実際に動かしてみて、実行にかかった時間を計測します。
> elapsed: 1.138 sec
こちらもJevと同じく5回計測します。
> elapsed: 1.308 sec
> elapsed: 1.046 sec
> elapsed: 1.136 sec
> elapsed: 1.204 sec
> elapsed: 1.084 sec
1秒と少しくらいの時間がかかっています。
たしかにJevのほうが速いのですが、時間で言えば2倍くらいのように見えます。
まとめ
簡単な実装で、JevをStrandsに組み込むことができました。
Jevとの通信にかかる時間が長いので、日本で動かすと0.6秒ほどかかります。BedrockのNovaで頑張ると1秒程度でレスポンスが返るので、Jevの代替案としてBedrockを使うこともできるかも、という印象でした。