stablyai/orca · warning · Error

pet.json is invalid.

Error message

pet.json is invalid.

What it means

Thrown by the pet:importPetBundle handler when stat(manifestPath) succeeds but either manifestStat.isFile() is false (pet.json is a directory/device) or manifestStat.size already exceeds MAX_MANIFEST_BYTES (64 KiB, defined as 64 * 1024). The size pre-check is an early guard; a second TOCTOU check at line 276 re-verifies after reading. pet.json is spec'd to be tiny, so an oversized manifest is treated as malicious/stuffed.

Source

Thrown at src/main/ipc/pet.ts:265

    }
    const picked = result.filePaths[0]
    let bundleDir: string
    try {
      const pickedStat = await stat(picked)
      bundleDir = pickedStat.isDirectory() ? picked : dirname(picked)
    } catch {
      throw new Error('Could not read the selected path.')
    }

    const manifestPath = join(bundleDir, 'pet.json')
    let manifestStat: Awaited<ReturnType<typeof stat>>
    try {
      manifestStat = await stat(manifestPath)
    } catch {
      throw new Error('Bundle is missing pet.json.')
    }
    if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) {
      throw new Error('pet.json is invalid.')
    }
    if (await isSymlink(manifestPath)) {
      throw new Error('pet.json must not be a symlink.')
    }

    let manifest: ResolvedPetManifest<PetManifest>
    try {
      const raw = await readFile(manifestPath, 'utf8')
      // Why: defend against TOCTOU — the file may have grown between the stat check and this read.
      if (Buffer.byteLength(raw, 'utf8') > MAX_MANIFEST_BYTES) {
        throw new Error('pet.json exceeded the manifest size limit.')
      }
      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.

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Keep pet.json small — reference spritesheetPath rather than embedding image bytes.
  2. Confirm pet.json is a regular file, not a directory or device node.
  3. Trim oversized animations maps and long description fields (description is capped at 2000 chars by schema anyway).
  4. If you genuinely need a large manifest, that exceeds the format — restructure so binary assets live as separate files.
Defensive patterns

Strategy: validation

Validate before calling

import { stat } from 'node:fs/promises'
import { join } from 'node:path'
const MAX_MANIFEST_BYTES = 64 * 1024

const s = await stat(join(bundleDir, 'pet.json'))
if (!s.isFile() || s.size > MAX_MANIFEST_BYTES) {
  notify('pet.json is invalid or too large (max 64 KiB).')
  return
}

Try / catch

try {
  await ipcRenderer.invoke('pet:importPetBundle')
} catch (e) {
  if (e instanceof Error && e.message === 'pet.json is invalid.') {
    notify('pet.json is the wrong type or exceeds 64 KiB. Reference external spritesheetPath instead of embedding data.')
  } else throw e
}

Prevention

When it happens

Trigger: pet.json is actually a directory (e.g. the user named a folder 'pet.json'), or pet.json is larger than 64 KiB due to embedded base64, a huge animations map, or padding/junk appended to the file.

Common situations: A bundle generator embedded a base64-encoded spritesheet inside pet.json instead of referencing spritesheetPath; a hand-edited manifest grew past 64 KiB; or the manifest was corrupted/padded.

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/6d54aee592caaeb6. Report an issue: GitHub.