stablyai/orca · error · Error
spritesheetPath must point to a file, not the bundle root.
Error message
spritesheetPath must point to a file, not the bundle root.
What it means
Thrown at pet.ts:295-296 when resolve(bundleDir, normalizedSpritePath) equals resolve(bundleDir) — meaning spritesheetPath resolves to the bundle directory itself (empty or all-separator value), not a file. This guards the later directory-walking and copy logic from being pointed at a directory root.
Source
Thrown at src/main/ipc/pet.ts:296
manifest = applyCodexPetDefaults(PetManifestSchema.parse(JSON.parse(raw)))
} catch (error) {
throw new Error(`Invalid pet.json: ${error instanceof Error ? error.message : 'parse error'}`)
}
// Why: spritesheetPath is bundle-relative and attacker-controlled — reject absolute/escaping paths (and symlinks) so a bundle can't reach outside.
const normalizedSpritePath = manifest.spritesheetPath.replace(/[\\/]+/g, sep)
if (
isAbsolute(manifest.spritesheetPath) ||
isAbsolute(normalizedSpritePath) ||
/^[a-zA-Z]:/.test(manifest.spritesheetPath)
) {
throw new Error('spritesheetPath must be relative to the bundle.')
}
// Why: bundles exported on Windows may be imported on macOS/Linux; normalize separators before resolving.
const sheetSrc = resolve(bundleDir, normalizedSpritePath)
const bundleResolved = resolve(bundleDir)
if (sheetSrc === bundleResolved) {
throw new Error('spritesheetPath must point to a file, not the bundle root.')
}
const bundleRoot = bundleResolved + sep
// Why: Windows volumes are case-insensitive; lowercase the prefix compare so case differences can't bypass the escape check.
const cmp = process.platform === 'win32' ? (s: string) => s.toLowerCase() : (s: string) => s
if (!cmp(sheetSrc + sep).startsWith(cmp(bundleRoot))) {
throw new Error('spritesheetPath escapes the bundle.')
}
if (await isSymlink(sheetSrc)) {
throw new Error('spritesheet must not be a symlink.')
}
const sheetClass = classifyFile(sheetSrc)
if (!sheetClass || sheetClass.ext === '.svg') {
// SVG can't be used as a sprite sheet (no pixel grid).
throw new Error('Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.')
}
let sheetStat: Awaited<ReturnType<typeof stat>>
try {
sheetStat = await stat(sheetSrc)View on GitHub (pinned to 1136503c6a)
Solutions
- Set spritesheetPath to the actual image filename inside the bundle, e.g. "spritesheet.png".
- Ensure the value is neither empty nor '.' and points to a real file inside the bundle directory.
Example fix
// before
{ "spritesheetPath": "." }
// after
{ "spritesheetPath": "spritesheet.png" } Defensive patterns
Strategy: validation
Validate before calling
import { resolve } from 'node:path'
function assertSheetIsNotBundleRoot(bundleDir: string, spritePath: string) {
const normalized = spritePath.replace(/[\\/]+/g, '/')
if (resolve(bundleDir, normalized) === resolve(bundleDir)) {
throw new Error('spritesheetPath resolves to the bundle root')
}
} Try / catch
try { await importPetBundle(p) }
catch (e) { if (e instanceof Error && e.message === 'spritesheetPath must point to a file, not the bundle root.') { /* fix manifest */ } else throw e } Prevention
- Ensure spritesheetPath is a non-empty filename, never '.' or a bare separator.
- Validate the manifest in your export tool before shipping the bundle.
When it happens
Trigger: pet.json sets spritesheetPath to '.', '', '.\', or a value that normalizes away to nothing so that resolve collapses it to bundleDir. Also reachable if the path is all slashes.
Common situations: Bundle generator emits an empty spritesheetPath default; a hand-edit sets '.' by mistake; the field was trimmed to empty.
Related errors
- Invalid pet.json: ${error instanceof Error ? error.message :
- spritesheetPath must be relative to the bundle.
- spritesheetPath escapes the bundle.
- Spritesheet must be a PNG, APNG, JPG, GIF, or WebP.
- Spritesheet path is not a file.
AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12).
Data as JSON: /api/errors/fab0077810260081.
Report an issue: GitHub.