stablyai/orca · error · Error

Invalid pet.json: ${error instanceof Error ? error.message :

Error message

Invalid pet.json: ${error instanceof Error ? error.message : 'parse error'}

What it means

Thrown when importing a pet bundle whose pet.json cannot be read, parsed as JSON, or validated against PetManifestSchema (Zod). It is a catch-all wrapper at pet.ts:278-281 that re-wraps any failure from readFile, JSON.parse, the MAX_MANIFEST_BYTES (64 KB) TOCTOU guard, or PetManifestSchema.parse into a single 'Invalid pet.json' message, appending the inner error's message when available. The inner cause is preserved in the message suffix, which is the real diagnostic.

Source

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

      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.
    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

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Read the suffix after the colon — it is the inner Zod/error message and names the exact failing field (e.g. 'spritesheetPath: invalid spritesheetPath'). Fix that field in pet.json.
  2. Validate the manifest locally before import: const PetManifestSchema requires id/description/spritesheetPath bounds listed at pet.ts:77-113; run it through a JSON linter.
  3. Confirm pet.json is under 64 KB and contains no NUL bytes, leading '/', '\', or '..' segments in spritesheetPath.
  4. If the file was mid-write, re-export the bundle and retry the import.

Example fix

// before — pet.json with a trailing comma and oversized id
{
  "id": "<128+ chars>",
  "displayName": "Cat",
  "spritesheetPath": "cat.png",
}
// after — valid shape within Zod bounds
{
  "displayName": "Cat",
  "spritesheetPath": "cat.png",
  "frame": { "width": 32, "height": 32 }
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate pet.json against the same shape before invoking the import IPC.
import { z } from 'zod'
const PetManifestSchema = z.object({
  id: z.string().min(1).max(128).optional(),
  displayName: z.string().min(1).max(120).optional(),
  description: z.string().max(2000).optional(),
  spritesheetPath: z.string().min(1).max(255)
    .refine((p) => !p.includes('\0') && !p.startsWith('/') && !p.startsWith('\\') && !p.includes('..'), 'invalid spritesheetPath')
    .optional(),
  frame: z.object({ width: z.number().int().positive().max(1024), height: z.number().int().positive().max(1024) }).optional(),
  fps: z.number().positive().max(60).optional(),
  defaultAnimation: z.string().min(1).max(64).optional(),
  animations: z.record(z.string().min(1).max(64), z.object({
    row: z.number().int().min(0).max(256),
    frames: z.number().int().positive().max(512),
    frameDurationsMs: z.array(z.number().positive().max(60_000)).max(512).optional()
  })).optional()
}).loose()

async function preflightManifest(path: string) {
  const raw = await readFile(path, 'utf8')
  if (Buffer.byteLength(raw, 'utf8') > 64 * 1024) throw new Error('manifest too large')
  return PetManifestSchema.parse(JSON.parse(raw))
}

Type guard

function isPetManifest(v: unknown): v is PetManifest {
  return typeof v === 'object' && v !== null &&
    (v.spritesheetPath === undefined || (typeof v.spritesheetPath === 'string' && v.spritesheetPath.length <= 255))
}

Try / catch

try {
  await importPetBundle(pickedPath)
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid pet.json:')) {
    // surface the inner cause (after the colon) to the user for the specific field fix
    showError(e.message.slice('Invalid pet.json:'.length).trim())
  } else throw e
}

Prevention

When it happens

Trigger: Calling the pet:import IPC handler (or whichever path invokes the bundle-import function) with a directory whose pet.json is truncated/malformed JSON, references unknown keys in strict mode, has a field outside Zod bounds (e.g. id longer than 128 chars, spritesheetPath with a NUL byte or '..'), or a raw file larger than MAX_MANIFEST_BYTES (64*1024) that grew between the earlier stat check and the readFile at line 273.

Common situations: Hand-edited pet.json with a trailing comma or unquoted value; bundle exported by a generator emitting a field the schema doesn't expect combined with a future schema tightening; manifest accidentally including megabytes of base64 metadata; encoding issue (UTF-16 BOM) making JSON.parse throw; a partially written file from a crashed export.

Understand the failure class

Related errors


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