iflytek/astron-agent · error · GateError

Docker Compose config returned an unexpected JSON root

Error message

Docker Compose config returned an unexpected JSON root

What it means

After successfully parsing Compose config output as JSON, _render_compose checks the root value is a dict (Mapping) because the script expects the standard Compose config document shape {services: ...}. If json.loads yields a list, string, number or null, it raises GateError('Docker Compose config returned an unexpected JSON root').

Solutions

  1. Verify the real Compose v2 binary is invoked (docker compose version; avoid shims/wrappers) since it always emits an object root for config --format json.
  2. Remove any wrapper scripts or output post-processing around the compose command in PATH.
  3. Inspect the raw output: echo the stdout to a file and check its root type with python -c "import json,sys; print(type(json.load(open('out.json'))))".
  4. If a wrapper is required, make it pass through the object unchanged.

Example fix

// before (wrapper emits list)
[{"service":"x"}]  # GateError: unexpected JSON root
// after (pass-through)
exec /usr/libexec/docker/cli-plugins/docker-compose "$@"
Defensive patterns

Strategy: type-guard

Validate before calling

import json, subprocess
out = subprocess.run(["docker","compose","-f",f,"config","--format","json"], capture_output=True, text=True).stdout
assert isinstance(json.loads(out), dict)

Type guard

def is_mapping_root(raw: str) -> bool:
    import json
    try:
        return isinstance(json.loads(raw), dict)
    except (json.JSONDecodeError, TypeError):
        return False

Try / catch

try:
    cfg = render_compose(f)
except GateError:
    cfg = None
if cfg is None:
    fail_with_diagnostics()

Prevention

When it happens

Trigger: 'compose config --format json' returns valid JSON whose top-level value is not an object — e.g. a nonstandard implementation, a wrapper emitting an array of documents, or intercepted output replaced with something else.

Common situations: Custom compose wrappers/shims that post-process output; hypothetically patched or bleeding-edge Compose builds changing output shape; a test double/mocked compose script returning the wrong structure.

Related errors


AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12). Data as JSON: /api/errors/db346a4e018da7fb. Report an issue: GitHub.

Appendix: source

Thrown at docker/astronAgent/scripts/verify_security_contract.py:186


def _render_compose(compose_file: Path) -> Mapping[str, Any]:
    base_command = _select_compose_command(compose_file)
    completed = subprocess.run(
        list(base_command) + ["-f", compose_file.name, "config", "--format", "json"],
        cwd=str(compose_file.parent),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        universal_newlines=True,
        check=False,
    )

    try:
        rendered = json.loads(completed.stdout)
    except (json.JSONDecodeError, TypeError) as exc:
        raise GateError("Docker Compose config did not return valid JSON") from exc
    if not isinstance(rendered, dict):
        raise GateError("Docker Compose config returned an unexpected JSON root")
    return rendered


def _mapping(value: Any) -> Mapping[str, Any]:
    return value if isinstance(value, dict) else {}


def _mounts(service: Mapping[str, Any], target: str) -> List[Mapping[str, Any]]:
    volumes = service.get("volumes", [])
    if not isinstance(volumes, list):
        return []
    return [
        volume
        for volume in volumes
        if isinstance(volume, dict) and volume.get("target") == target
    ]

View on GitHub (pinned to 5e758547a8)