Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Keeping an agent inside one folder

After this lesson: I can keep an agent inside one folder by checking the resolved path.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

Keeping an agent in its folder

A sandbox folder is the one folder an agent may read and write in. Every path it asks for must end up inside that folder.

That is harder than it sounds. A path can climb out with two dots, or go through a link that points somewhere else. The fix is to check where a path really ends up.

By the end of this lesson

Note

I can keep an agent inside one folder by checking the resolved path.

Key words

WordWhat it meansExample
Sandbox folderthe one folder an agent may use
Resolveto work out where a path really goes
Linka file that points at another placeshortcut
Escapeto end up outside the folder
Parentthe folder above, written as two dots..

The cells students fill in are left blank in this preview.

Resolve first, then compare

Resolving a path means following every two dots and every link until you have the real address. Only then do you ask: is this inside the folder?

Compare with a slash on the end. The folder box is not the folder box-old, though one name starts with the other.

Worked example: folder /work/box

Asked forReally goes toInside?
a.txt/work/box/a.txtYes
../outside/s.txt/work/outside/s.txtNo
/etc/hosts/etc/hostsNo
../box-old/x.txt/work/box-old/x.txtNo
shortcut/s.txt/work/outside/s.txtNo: shortcut is a link

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think removing the two dots from a path makes it safe. It does not. A link inside the folder can point outside, so the text looks safe and the real place is not. Always check the real path, not the text.

If you cannot resolve it, refuse

A path may point at a folder that does not exist, or at links that loop for ever. You cannot say where it goes, so you cannot say it is safe.

Refuse it. A refusal costs the agent one step. A wrong yes can cost a whole disk.

Write the model helper, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the model helper, part 2

Command
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(\"Recorded sample: no model answered (\" + error.message + \").\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask };' >> model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the folder check, part 1

Command
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"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the folder check, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const root = fs.realpathSync(box);' '// Where a path really goes: follow links, then compare with the folder.' '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"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the folder check, part 3

Command
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 + \" text-only:\" + naive(p) + \" resolved:\" + inside(p)); });' '  console.log(\"files in box: \" + fs.readdirSync(box).sort().join(\" \"));' '  const q = \"In one short sentence, why must a path be resolved before it is checked?\";' '  const sample = \"A path can use dots or a link to point somewhere else, so you check where it really ends up.\";' '  const reply = await ask([{ role: \"user\", content: q }], sample, 60);' '  console.log(\"model said: \" + reply.text);' '}' 'main();' >> confine.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the folder check

Command
docker exec -w /home/node/project agent node confine.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

What the Run prints

LineWhat it shows
a.txt text-only:true resolved:trueA file inside the folder
../outside/s.txt text-only:false ...Both checks catch the dots
../box-old/x.txt text-only:true resolved:falseA name that starts the same fools the text
shortcut/s.txt text-only:true resolved:falseA real link out of the folder
files in box: a.txt shortcutThe Run made these files for real
model said: ...A sentence. The words differ each run

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.