moeru-ai/airi · error · Error
Only screenshots from this AIRI computer-use store can be…
Error message
Only screenshots from this AIRI computer-use store can be read.
What it means
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.
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
Example fix
// before
await readImage({ path: '/Users/me/Pictures/shot.png' })
// after
const { artifactPath } = await run({ argv: ['invoke', 'screen.capture'] })
await readImage({ path: artifactPath }) Defensive patterns
Strategy: validation
Validate before calling
import { realpath, stat } from 'node:fs/promises'
import { relative, isAbsolute, sep } from 'node:path'
async function isInsideStore(root: string, p: string): Promise<boolean> {
const [r, t] = await Promise.all([realpath(root), realpath(p)])
const rel = relative(r, t)
return Boolean(rel) && !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)
} Try / catch
try {
return await readImage({ path })
} catch (e) {
if (e instanceof Error && e.message.includes('Only screenshots from this AIRI computer-use store')) {
console.warn('Path escapes the store; capture via the CLI to get a valid artifact path')
return null
}
throw e
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Extension entrypoint escapes the package folder
- Extension entrypoint resolves outside the package folder
- Extension packages can contain only files and directories
- Extension packages cannot contain symbolic links
- Extension source must be a regular directory
AI-assisted analysis of moeru-ai/airi@438a067dde (2026-09-17).
Data as JSON: /api/errors/eac2d95757a8b8ac.
Report an issue: GitHub.
Appendix: source
Thrown at apps/stage-tamagotchi/src/main/services/airi/computer-use/runtime.ts:143
let output: unknown = stdout.trim()
try {
output = JSON.parse(stdout)
}
catch {
// Help output is text. Keep it intact for command discovery.
}
return { argv, exitCode, output, stderr }
})
}
async function readImage(input: unknown): Promise<string> {
const { path } = v.parse(v.object({ path: v.string() }), input)
return enqueue(async () => {
const root = await realpath(options.storeRoot)
const target = await realpath(path)
const within = relative(root, target)
if (!within || isAbsolute(within) || within === '..' || within.startsWith(`..${sep}`))
throw new Error('Only screenshots from this AIRI computer-use store can be read.')
const info = await stat(target)
if (!info.isFile() || info.size > 8 * 1024 * 1024)
throw new Error('Screenshot must be a file smaller than 8 MiB.')
const data = await readFile(target)
const png = data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
const jpeg = data[0] === 255 && data[1] === 216 && data[2] === 255
if (!png && !jpeg)
throw new Error('Screenshot must be PNG or JPEG.')
return `data:image/${png ? 'png' : 'jpeg'};base64,${data.toString('base64')}`
})
}
/** Rejects queued calls, aborts active work, and waits for the owned daemon to exit. */
async function dispose() {
disposed = true
abort.abort()
await queue
try {View on GitHub (pinned to 438a067dde)