{"record":{"id":"eac2d95757a8b8ac","repo":"moeru-ai/airi","slug":"only-screenshots-from-this-airi-computer-use-store-can-be","errorCode":null,"errorMessage":"Only screenshots from this AIRI computer-use store can be read.","messagePattern":"Only screenshots from this AIRI computer-use store can be read\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/stage-tamagotchi/src/main/services/airi/computer-use/runtime.ts","lineNumber":143,"sourceCode":"      let output: unknown = stdout.trim()\n      try {\n        output = JSON.parse(stdout)\n      }\n      catch {\n        // Help output is text. Keep it intact for command discovery.\n      }\n      return { argv, exitCode, output, stderr }\n    })\n  }\n\n  async function readImage(input: unknown): Promise<string> {\n    const { path } = v.parse(v.object({ path: v.string() }), input)\n    return enqueue(async () => {\n      const root = await realpath(options.storeRoot)\n      const target = await realpath(path)\n      const within = relative(root, target)\n      if (!within || isAbsolute(within) || within === '..' || within.startsWith(`..${sep}`))\n        throw new Error('Only screenshots from this AIRI computer-use store can be read.')\n      const info = await stat(target)\n      if (!info.isFile() || info.size > 8 * 1024 * 1024)\n        throw new Error('Screenshot must be a file smaller than 8 MiB.')\n      const data = await readFile(target)\n      const png = data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))\n      const jpeg = data[0] === 255 && data[1] === 216 && data[2] === 255\n      if (!png && !jpeg)\n        throw new Error('Screenshot must be PNG or JPEG.')\n      return `data:image/${png ? 'png' : 'jpeg'};base64,${data.toString('base64')}`\n    })\n  }\n\n  /** Rejects queued calls, aborts active work, and waits for the owned daemon to exit. */\n  async function dispose() {\n    disposed = true\n    abort.abort()\n    await queue\n    try {","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/apps/stage-tamagotchi/src/main/services/airi/computer-use/runtime.ts#L125-L161","documentation":"readImage() resolves both the store root and the requested path with realpath and allows reads only for files that resolve strictly inside the AIRI computer-use store directory. This is a symlink-escape/path-traversal guard: paths outside the store, the root itself, or paths reached via symlink are rejected.","triggerScenarios":"Passing a path outside options.storeRoot, passing the store root itself, a path containing '..' that escapes the root, or a symlink inside the store pointing to an external file.","commonSituations":"Trying to reuse readImage as a generic image reader; the CLI writing screenshots to a different store root than the host configured; swapped symlinks on the filesystem.","solutions":["Read only screenshot files the CLI wrote under app.getPath('userData')/computer-use (the configured storeRoot)","Capture the path returned by the screenshot command instead of constructing your own","Verify the resolved (realpath) location of the file is inside the store before calling readImage"],"exampleFix":"// before\nawait readImage({ path: '/Users/me/Pictures/shot.png' })\n// after\nconst { artifactPath } = await run({ argv: ['invoke', 'screen.capture'] })\nawait readImage({ path: artifactPath })","handlingStrategy":"validation","validationCode":"import { realpath, stat } from 'node:fs/promises'\nimport { relative, isAbsolute, sep } from 'node:path'\nasync function isInsideStore(root: string, p: string): Promise<boolean> {\n  const [r, t] = await Promise.all([realpath(root), realpath(p)])\n  const rel = relative(r, t)\n  return Boolean(rel) && !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await readImage({ path })\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Only screenshots from this AIRI computer-use store')) {\n    console.warn('Path escapes the store; capture via the CLI to get a valid artifact path')\n    return null\n  }\n  throw e\n}","preventionTips":["Always use artifact paths returned by the screenshot command","Never construct store paths by hand or accept user-supplied paths","Remember realpath: symlinks pointing outside the store will be rejected"],"tags":["security","path-traversal","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}