Un agente dentro de su carpeta
Una carpeta confinada es la única carpeta donde un agente puede leer y escribir. Cada ruta que pida debe acabar dentro de ella.
Es más difícil de lo que parece. Una ruta puede salir con dos puntos, o pasar por un enlace que apunta a otro sitio. La solución es comprobar adónde acaba de verdad la ruta.
Al final de esta lección
Nota
Puedo mantener a un agente dentro de una carpeta comprobando la ruta resuelta.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Carpeta confinada | la única carpeta que un agente puede usar | |
| Resolver | calcular adónde va de verdad una ruta | |
| Enlace | un archivo que apunta a otro lugar | atajo |
| Escapar | acabar fuera de la carpeta | |
| Carpeta superior | la carpeta de arriba, escrita con dos puntos | .. |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Primero resuelve, después compara
Resolver una ruta es seguir cada par de puntos y cada enlace hasta tener la dirección real. Solo entonces preguntas: ¿está dentro de la carpeta?
Compara con una barra al final. La carpeta caja no es la carpeta caja-vieja, aunque un nombre empiece como el otro.
Ejemplo resuelto: carpeta /trabajo/caja
| Se pide | Va de verdad a | ¿Dentro? |
|---|---|---|
| nota.txt | /trabajo/caja/nota.txt | Sí |
| ../fuera/clave.txt | /trabajo/fuera/clave.txt | No |
| /etc/hosts | /etc/hosts | No |
| ../caja-vieja/x.txt | /trabajo/caja-vieja/x.txt | No |
| atajo/clave.txt | /trabajo/fuera/clave.txt | No: atajo es un enlace |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error frecuente
Advertencia
Mucha gente cree que quitar los dos puntos de una ruta la hace segura. No es así. Un enlace dentro de la carpeta puede apuntar fuera: el texto parece seguro y el lugar real no. Comprueba siempre la ruta real, no el texto.
Si no puedes resolverla, rechaza
Una ruta puede apuntar a una carpeta que no existe, o a enlaces que dan vueltas sin fin. No puedes decir adónde va, así que no puedes decir que sea segura.
Recházala. Un rechazo le cuesta al agente un paso. Un sí equivocado puede costar un disco entero.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 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(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la comprobación de carpeta, parte 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, \"caja\");' 'fs.rmSync(base, { recursive: true, force: true });' 'fs.mkdirSync(box, { recursive: true });' 'fs.mkdirSync(path.join(base, \"caja-vieja\"));' 'fs.mkdirSync(path.join(base, \"fuera\"));' 'fs.writeFileSync(path.join(box, \"nota.txt\"), \"dentro\");' 'fs.writeFileSync(path.join(base, \"fuera\", \"clave.txt\"), \"secreto\");' 'fs.symlinkSync(path.join(base, \"fuera\"), path.join(box, \"atajo\"));' > confine.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la comprobación de carpeta, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const root = fs.realpathSync(box);' '// Adónde va de verdad una ruta: sigue los enlaces y compara con la carpeta.' '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"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe la comprobación de carpeta, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const asked = [\"nota.txt\", \"nueva.txt\", \"../fuera/clave.txt\", \"/etc/hosts\", \"../caja-vieja/x.txt\", \"atajo/clave.txt\"];' 'async function main() {' ' asked.forEach(function (p) { console.log(p + \" texto:\" + naive(p) + \" real:\" + inside(p)); });' ' console.log(\"archivos en caja: \" + fs.readdirSync(box).sort().join(\" \"));' ' const q = \"En una frase corta: ¿por qué hay que resolver una ruta antes de comprobarla?\";' ' const sample = \"Una ruta puede usar puntos o un enlace para apuntar a otro sitio, así que se comprueba dónde acaba de verdad.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"el modelo dijo: \" + reply.text);' '}' 'main();' >> confine.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecuta la comprobación de carpeta
docker exec -w /home/node/project agent node confine.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que imprime el Run
| Línea | Qué muestra |
|---|---|
| nota.txt texto:true real:true | Un archivo dentro de la carpeta |
| ../fuera/clave.txt texto:false ... | Las dos comprobaciones cazan los puntos |
| ../caja-vieja/x.txt texto:true real:false | Un nombre que empieza igual engaña al texto |
| atajo/clave.txt texto:true real:false | Un enlace real que sale de la carpeta |
| archivos en caja: atajo nota.txt | El Run creó estos archivos de verdad |
| el modelo dijo: ... | Una frase. Las palabras cambian en cada ejecución |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.