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

無料レッスン

一つのフォルダに閉じ込める

このレッスンのあとで: 解決したパスを確かめて、エージェントを一つのフォルダに閉じ込めることができる。

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

フォルダから出さない

サンドボックスフォルダは、エージェントが読み書きしてよい、たった一つのフォルダです。頼まれたパスは、すべてそのフォルダの中に行き着かなければいけません。

これは見た目より難しいです。パスは、ドット二つで外へ出られますし、別の場所を指すリンクも通れます。直し方は、パスが本当に行き着く先を確かめることです。

このレッスンのゴール

メモ

解決したパスを確かめて、エージェントを一つのフォルダに閉じ込めることができる。

キーワード

ことばいみれい
サンドボックスフォルダエージェントが使ってよい、一つのフォルダ
解決するパスが本当に行き着く先を求める
リンク別の場所を指すファイル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 shortcutRun が実際に作ったファイル
モデル: ...一文。言葉は毎回変わる

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

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

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