Error codes
Every error mvmforge emits to its structured output carries a stable code string. SDK tests can pattern-match on these codes; the strings are append-only within an IR major version per ADR-0004.
The canonical registry is schema/error-codes.json in the repo. CI cross-checks the registry against the ErrorCode enum in mvmforge-ir.
Format
Errors appear in the canonical-JSON envelope validate (and compile) emit:
{ "errors": [ { "code": "E_SECRETS_NOT_IMPLEMENTED", "path": ".apps[0].env.TOKEN", "detail": "SecretRef values are not supported until the secrets subsystem ADR lands" } ], "ok": false}codeis stable. Match on this in tests.pathis a JSON-Pointer-ish dotted path identifying where the error is. May be empty for whole-document errors.detailis human-readable. The text is not stable; do not match on it.
v0.1 codes
Schema-version errors
Path: .schema_version
| Code | Triggered when |
|---|---|
E_UNSUPPORTED_MAJOR | IR MAJOR is outside the host’s supported set. |
E_MINOR_TOO_HIGH | IR MINOR exceeds the host’s known minor for the supported MAJOR. |
E_MALFORMED_VERSION | schema_version isn’t a valid MAJOR.MINOR string. |
Manifest-shape errors
| Code | Triggered when |
|---|---|
E_UNKNOWN_FIELD | The manifest has a field not declared in the schema (closed-world). |
E_MALFORMED_MANIFEST | The file isn’t readable, isn’t valid JSON, or doesn’t deserialize as Workload. |
App-list errors
Path: .apps
| Code | Triggered when |
|---|---|
E_EMPTY_APPS | apps is empty. v0 requires at least one app. |
E_MULTI_APP_DEFERRED | apps has more than one entry. v0 supports exactly one app per workload. |
Per-app errors
Paths: .apps[i].source.kind, .apps[i].env.<KEY>, .apps[i].entrypoint.env.<KEY>, .apps[i].network.ports, .volumes[i].persist
| Code | Triggered when |
|---|---|
E_SOURCE_KIND_DEFERRED | app.source.kind is nix_derivation or oci_image. v0 implements only local_path. |
E_SECRETS_NOT_IMPLEMENTED | An EnvValue is secret_ref. The secrets subsystem awaits a dedicated ADR. |
E_NETWORK_PORTS_WITH_NONE | app.network.mode is "none" but ports is non-empty. |
E_PERSIST_DEFERRED | A Volume.persist is true. v0 doesn’t support persistent volumes. |
Compile-time errors
| Code | Triggered when |
|---|---|
E_COMPILE_OUTPUT_EXISTS_NOT_DIR | The --out path supplied to mvmforge compile exists but is not a directory. |
E_COMPILE_STAGING_FAILED | Atomic staging failed (cross-filesystem rename, permission, or I/O error). |
Source bundling errors
Per ADR-0008.
| Code | Triggered when |
|---|---|
E_SOURCE_PATH_NOT_FOUND | app.source.path does not exist on the compile host. |
E_SOURCE_PATH_NOT_DIR | app.source.path exists but is not a directory. |
E_SOURCE_COPY_FAILED | I/O failure copying source files into staging (permission, disk full, etc.). |
E_SOURCE_GLOB_INVALID | An entry in include or exclude is not a valid glob. |
E_SOURCE_OUT_OF_TREE_SYMLINK | A symlink in the source tree resolves to a target outside app.source.path. |
Substrate errors
| Code | Triggered when |
|---|---|
E_MVM_NOT_FOUND | The mvmctl binary could not be located via MVMFORGE_MVM_BIN or PATH. |
Versioning
Per ADR-0004:
- Append-only within an IR major version. New codes can be added; existing codes can’t be renamed or removed without a superseding ADR and an IR
MAJORbump. - String stability. The exact error-code string is the contract surface.
- Format stability. The enclosing JSON envelope shape (
{"errors": [...], "ok": bool}) and the per-error fields (code,path,detail) are stable.
Programmatic consumption
Python
import json, subprocess
result = subprocess.run( ["mvmforge", "validate", "manifest.json"], capture_output=True, text=True,)body = json.loads(result.stdout)if not body["ok"]: for err in body["errors"]: if err["code"] == "E_SECRETS_NOT_IMPLEMENTED": print(f"Secrets aren't supported yet at {err['path']}")TypeScript
import { execFileSync } from "node:child_process";
// Use execFileSync (no shell) with argv array — never execSync with a shell string.const out = execFileSync("mvmforge", ["validate", "manifest.json"], { encoding: "utf8",});const body = JSON.parse(out);for (const err of body.errors) { if (err.code === "E_SOURCE_PATH_NOT_FOUND") { console.error("Source path missing:", err.detail); }}Use execFileSync (or spawnSync with shell: false) for any programmatic invocation. execSync with a shell-string allows injection if any user-controlled value reaches the command line.