フォルダから出さない
サンドボックスフォルダは、エージェントが読み書きしてよい、たった一つのフォルダです。頼まれたパスは、すべてそのフォルダの中に行き着かなければいけません。
これは見た目より難しいです。パスは、ドット二つで外へ出られますし、別の場所を指すリンクも通れます。直し方は、パスが本当に行き着く先を確かめることです。
このレッスンのゴール
メモ
解決したパスを確かめて、エージェントを一つのフォルダに閉じ込めることができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| サンドボックスフォルダ | エージェントが使ってよい、一つのフォルダ | |
| 解決する | パスが本当に行き着く先を求める | |
| リンク | 別の場所を指すファイル | shortcut |
| 脱出 | フォルダの外へ行き着くこと | |
| 親 | 一つ上のフォルダ。ドット二つで書く | .. |
学生が入力するセルは、このプレビューでは空白です。
先に解決し、それから比べる
パスを解決するとは、ドット二つとリンクをすべてたどって、本当の行き先を求めることです。そのあとで初めて、フォルダの中かどうかを聞きます。
比べるときは、末尾にスラッシュを付けます。box と box-old は、片方がもう片方で始まっていても、別のフォルダです。
例: フォルダが /work/box
| 頼んだパス | 本当の行き先 | 中か |
|---|---|---|
| a.txt | /work/box/a.txt | はい |
| ../outside/s.txt | /work/outside/s.txt | いいえ |
| /etc/hosts | /etc/hosts | いいえ |
| ../box-old/x.txt | /work/box-old/x.txt | いいえ |
| shortcut/s.txt | /work/outside/s.txt | いいえ: shortcut はリンク |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
パスからドット二つを取り除けば安全になる、と考える人が多いです。そうはなりません。フォルダの中のリンクは、外を指せます。文字は安全に見えても、本当の場所は安全ではありません。文字ではなく、本当のパスを確かめましょう。
解決できなければ断る
パスが、存在しないフォルダを指していたり、リンクがいつまでも輪になっていたりすることがあります。行き先が言えないなら、安全だとも言えません。
断りましょう。断っても、エージェントは一歩を失うだけです。間違って許すと、ディスク全体を失うかもしれません。
助っ人 model.js を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答のトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
助っ人 model.js を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '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 || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
フォルダの確認を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const fs = require(\"fs\");' 'const path = require(\"path\");' 'const base = path.join(process.cwd(), \"sandbox-lab\");' 'const box = path.join(base, \"box\");' 'fs.rmSync(base, { recursive: true, force: true });' 'fs.mkdirSync(box, { recursive: true });' 'fs.mkdirSync(path.join(base, \"box-old\"));' 'fs.mkdirSync(path.join(base, \"outside\"));' 'fs.writeFileSync(path.join(box, \"a.txt\"), \"inside\");' 'fs.writeFileSync(path.join(base, \"outside\", \"s.txt\"), \"secret\");' 'fs.symlinkSync(path.join(base, \"outside\"), path.join(box, \"shortcut\"));' > confine.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
フォルダの確認を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const root = fs.realpathSync(box);' '// 本当の行き先: リンクをたどってから、フォルダと比べる。' 'function inside(p) {' ' try {' ' const t = path.resolve(root, p);' ' const real = fs.existsSync(t) ? fs.realpathSync(t) : path.join(fs.realpathSync(path.dirname(t)), path.basename(t));' ' return real === root || real.startsWith(root + path.sep);' ' } catch (e) { return false; }' '}' 'function naive(p) { return path.resolve(root, p).startsWith(root); }' >> confine.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
フォルダの確認を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const asked = [\"a.txt\", \"new.txt\", \"../outside/s.txt\", \"/etc/hosts\", \"../box-old/x.txt\", \"shortcut/s.txt\"];' 'async function main() {' ' asked.forEach(function (p) { console.log(p + \" 文字だけ:\" + naive(p) + \" 解決後:\" + inside(p)); });' ' console.log(\"box の中のファイル: \" + fs.readdirSync(box).sort().join(\" \"));' ' const q = \"短く一文で答えてください。パスを確かめる前に解決しなければならないのは、なぜですか。\";' ' const sample = \"パスはドットやリンクを使って別の場所を指せるので、本当に行き着く先を確かめるからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 80);' ' console.log(\"モデル: \" + reply.text);' '}' 'main();' >> confine.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
フォルダの確認を動かす
docker exec -w /home/node/project agent node confine.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| a.txt 文字だけ:true 解決後:true | フォルダの中のファイル |
| ../outside/s.txt 文字だけ:false ... | 二つの確認ともドットを見つける |
| ../box-old/x.txt 文字だけ:true 解決後:false | 始まりが同じ名前は、文字だけだとだまされる |
| shortcut/s.txt 文字だけ:true 解決後:false | フォルダから外へ出る本物のリンク |
| box の中のファイル: a.txt shortcut | Run が実際に作ったファイル |
| モデル: ... | 一文。言葉は毎回変わる |
学生が入力するセルは、このプレビューでは空白です。