クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

AIエージェントの混乱した代理人

このレッスンのあとで: 頼んだ人より大きな権限でエージェントが動かないよう、操作ごとに依頼者と照らして確かめられる。

70分のレッスンコマンド8件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

混乱した代理人とは何か?

混乱した代理人とは、権限を持つプログラムが、権限のない人のために使うよう言いくるめられることです。エージェントは設計上、代理人です。ツールとアカウントを使って、人のために動きます。

エージェントが広い鍵を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語と短い理由。言葉は毎回変わります
プログラムの判定毎回「拒否」。表に田中さんの給与の権利がないため
記録済みサンプルモデルが答えないとき、最初に出ます。サンプルで続きます

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。