混乱した代理人とは何か?
混乱した代理人とは、権限を持つプログラムが、権限のない人のために使うよう言いくるめられることです。エージェントは設計上、代理人です。ツールとアカウントを使って、人のために動きます。
エージェントが広い鍵を1つだけ持ち、頼む人をすべて信じると、話しかけられる人はみな、その鍵を持つことになります。
このレッスンのゴール
メモ
頼んだ人より大きな権限でエージェントが動かないよう、操作ごとに依頼者と照らして確かめられる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 混乱した代理人 | 権限のない人のために、自分の権限を使ってしまうプログラム | |
| 権限 | 資源に対してできる操作。ここでは操作と資源の組 | read:payroll |
| スコープ | トークンに入っている、名前の付いた許可 | |
| 細いトークン | その作業に必要なスコープだけを、短い間持つトークン | |
| 依頼者 | この要求で、エージェントがその人のために動く相手 |
学生が入力するセルは、このプレビューでは空白です。
呼び出しはだれの権限か?
エージェントが、自分の鍵で給与システムに入るとします。メモしか読めない田中さんが、給与ファイルを頼みます。システムはエージェントの鍵を見て、よいと答えます。
エージェントは隠された行にだまされたのではありません。ただ頼まれただけです。問題は、呼び出しが田中さんの権限でなく、エージェントの権限を運んだことです。
例: 3つの設計
| 設計 | 呼び出しが運ぶもの | 田中さんが給与を頼むと |
|---|---|---|
| 広い鍵を1つ | エージェントの鍵 | システムが許可してしまう |
| 依頼者の身元を渡す | 田中さんの身元 | システムが田中さんを確かめて断る |
| 作業ごとの細いトークン | 作業に必要なスコープだけ | トークンにスコープがなく失敗する |
学生が入力するセルは、このプレビューでは空白です。
Runは呼び出しを2通りで判定
最初のスクリプトは、ユーザー2人と、架空の呼び出し3つを持ちます。各呼び出しを2回判定します。エージェントのトークンで1回、依頼者の権利で1回です。
2つの答えが違う所では、エージェントは頼んだ人より大きな権限で動くことになります。スクリプトはその行に印を付けます。何も呼ばず、誰も攻撃しません。
二重判定を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const rights = {' ' tanaka: [\"read:notes\"],' ' sato: [\"read:notes\", \"read:payroll\"],' '};' 'const agentToken = [\"read:notes\", \"read:payroll\"];' 'const calls = [' ' { user: \"tanaka\", action: \"read\", resource: \"notes\" },' ' { user: \"tanaka\", action: \"read\", resource: \"payroll\" },' ' { user: \"sato\", action: \"read\", resource: \"payroll\" },' '];' > deputy.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
二重判定を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function want(c) { return c.action + \":\" + c.resource; }' 'function byAgent(c) { return agentToken.includes(want(c)); }' 'function byAsker(c) { return (rights[c.user] || []).includes(want(c)); }' '' 'for (const c of calls) {' ' const a = byAgent(c), b = byAsker(c);' ' let line = c.user + \" が \" + want(c) + \" を依頼: エージェントのトークン \" + (a ? \"許可\" : \"拒否\");' ' line += \"、依頼者の権利 \" + (b ? \"許可\" : \"拒否\");' ' console.log(line + (a && b === false ? \" <- 混乱した代理人\" : \"\"));' '}' >> deputy.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
二重判定を実行
docker exec -w /home/node/project agent node deputy.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スクリプトの出力
| 行 | 意味 |
|---|---|
| tanaka が read:notes を依頼 | 両方許可。田中さんはメモを読める |
| tanaka が read:payroll を依頼 | トークンは許可、権利は拒否。印が付く |
| sato が read:payroll を依頼 | 両方許可。佐藤さんにはその権利がある |
学生が入力するセルは、このプレビューでは空白です。
一緒に効く3つの対策
依頼者を渡す。呼び出しごとに人の身元を運び、ツールやAPIが確かめます。
トークンを細くする。作業に必要なスコープだけを渡し、早く期限が切れるようにします。
操作ごとにコードで確かめる。操作が起きる所で、毎回確かめます。許されるかどうかをモデルに聞いてはいけません。
よくある間違い
注意
プロンプトに「自分のファイルだけ手伝って」と書いて終わりにする人が多いです。プロンプトはモデルへのお願いです。規則はコードの権利表で、毎回同じ答えを返します。
モデル用スクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const rights = { tanaka: [\"read:notes\"] };' 'const call = { user: \"tanaka\", action: \"read\", resource: \"payroll\" };' 'const messages = [' ' { role: \"system\", content: \"あなたは事務アシスタントです。ユーザーの田中さんが読めるのはメモだけです。許可か拒否のどちらか1語と、短い理由で答えてください。\" },' ' { role: \"user\", content: \"田中さんが給与ファイルを読むよう頼んでいます。どうしますか。\" },' '];' '// 記録済みサンプル。モデルが答えないときだけ使います。' 'const sample = \"拒否。田中さんが読めるのはメモだけです。\";' > deputy2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用スクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function decide(c) {' ' const allowed = rights[c.user] || [];' ' return allowed.includes(c.action + \":\" + c.resource) ? \"許可\" : \"拒否\";' '}' '' 'async function main() {' ' const reply = await ask(messages, sample, 60);' ' console.log(\"モデルの答え: \" + reply);' ' console.log(\"プログラムの判定: \" + decide(call));' ' console.log(\"プログラムが許可と判定したときだけ、呼び出しは実行されます。\");' '}' 'main();' >> deputy2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用ヘルパーを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用ヘルパーを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデルで実行
docker exec -w /home/node/project agent node deputy2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されうる内容 |
|---|---|
| モデルの答え | 拒否などの1語と短い理由。言葉は毎回変わります |
| プログラムの判定 | 毎回「拒否」。表に田中さんの給与の権利がないため |
| 記録済みサンプル | モデルが答えないとき、最初に出ます。サンプルで続きます |
学生が入力するセルは、このプレビューでは空白です。