jdx/mise · error · eyre::Report

task action manifest is not canonical JSON

Error message

task action manifest is not canonical JSON

What it means

parse_task_manifest deserializes a TaskActionManifest and, when require_canonical=true (remote fetches and persisted local copies), requires the raw bytes to byte-equal canonical_json(&manifest) — compact, deterministically key-ordered JSON. Any drift from that canonical serialization is rejected as tampering or writer skew.

Source

Thrown at crates/mise-cache-core/src/agent.rs:593

    fn load_task_manifest(&self, task: &str) -> Result<Option<TaskActionManifest>> {
        match fs::read(self.task_manifest_path(task)) {
            Ok(contents) => Ok(Some(self.parse_task_manifest(task, &contents, false)?)),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(error) => Err(error.into()),
        }
    }

    fn parse_task_manifest(
        &self,
        task: &str,
        contents: &[u8],
        require_canonical: bool,
    ) -> Result<TaskActionManifest> {
        let manifest: TaskActionManifest = serde_json::from_slice(contents)?;
        validate_task_manifest(&manifest, task)?;
        if require_canonical && canonical_json(&manifest)? != contents {
            bail!("task action manifest is not canonical JSON");
        }
        Ok(manifest)
    }

    fn task_manifest_selector(task: &str) -> Result<(Vec<u8>, CacheDigest)> {
        let bytes = canonical_json(&TaskActionManifestSelector {
            version: 1,
            kind: "task_action_manifest",
            task,
        })?;
        let digest = CacheDigest::blake3(&bytes);
        Ok((bytes, digest))
    }

    fn persist_task_manifest(&self, manifest: &TaskActionManifest) -> Result<()> {
        let bytes = canonical_json(manifest)?;
        fs::create_dir_all(self.manifest_dir.as_path())?;
        let mut temporary = tempfile::NamedTempFile::new_in(self.manifest_dir.as_path())?;

View on GitHub (pinned to 6f52dcdf99)

Solutions

  1. Regenerate the manifest instead of editing it: delete the local (or remote) copy and let the next task run repopulate it
  2. If you must write manifests programmatically, serialize with sorted keys and compact formatting to match canonical_json
  3. Upgrade writer and reader to the same mise-cache-core version so serialization matches
Defensive patterns

Strategy: fallback

Try / catch

// Treat non-canonical manifests as cache misses: regenerate rather than trust
match agent.begin_task(&task).await {
    Ok(run) => run,
    Err(e) if e.to_string().contains("not canonical JSON") => {
        remove_task_manifest(&task)?; // drop the mutated file
        agent.begin_task(&task).await?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: get_remote_task_manifest receiving bytes produced by a writer that used pretty-printing, unsorted maps, or an extra newline; hand-editing the local manifest file; a different serializer version reordering fields.

Common situations: Running jq/prettier or an editor that 'fixes' cache JSON files; version skew between writer and reader agents; a shared remote bucket written by tooling with different serialization.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/17c88d793ff192b9. Report an issue: GitHub.