iflytek/astron-agent · error · GateError

credential lifecycle shell command is incomplete

Error message

credential lifecycle shell command is incomplete

What it means

exercise_shell_credential_lifecycle reads the compose service's 'command' and expects a list of at least 3 elements whose third element is the inline shell script (string) that exercises the credential lifecycle. If the rendered compose config lacks such a command (wrong type, too short, or command[2] not a string), it raises GateError('credential lifecycle shell command is incomplete'), because it cannot extract and run the script after unescaping $$.

Solutions

  1. Restore the service command in the compose file to a 3-element list form: ["bash", "-c", "<script with $$-escaped $>"] and re-run the gate.
  2. Validate locally with 'docker compose config --format json | jq ".services.<name>.command"' to see the parsed type/shape.
  3. Quote the script argument in YAML (block scalar |) so YAML does not coerce it to a non-string.
  4. If the entrypoint pattern changed intentionally, update exercise_shell_credential_lifecycle to match the new command shape.

Example fix

# before
command: /entrypoint.sh
# after
command:
  - bash
  - -c
  - |
    export TENANT_DB_PASSWORD=$$TENANT_DB_PASSWORD
    ...
Defensive patterns

Strategy: validation

Validate before calling

import json, subprocess
cfg = json.loads(subprocess.run(["docker","compose","-f",f,"config","--format","json"], capture_output=True, text=True).stdout)
cmd = cfg["services"]["workflow"].get("command")
assert isinstance(cmd, list) and len(cmd) >= 3 and isinstance(cmd[2], str), f"bad command: {cmd!r}"

Type guard

def has_shell_command(command) -> bool:
    return (isinstance(command, list) and len(command) >= 3
            and isinstance(command[2], str))

Try / catch

try:
    exercise_shell_credential_lifecycle(cfg)
except GateError as e:
    if "shell command is incomplete" in str(e):
        print(json.dumps(service.get("command")))  # inspect actual shape
    raise

Prevention

When it happens

Trigger: The workflow/tenant service in the compose file defines 'command' as a string instead of a list, an array with fewer than 3 items, or with a non-string third element (e.g. YAML folded the script into a number/list).

Common situations: Refactoring the compose file's entrypoint/command; YAML type coercion (e.g. numeric-looking script start); a service image changed so its command was simplified and the security-contract assumptions broke.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

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

        mutate(candidate, marker)
        errors = validate_contract(candidate)
        if not any(error.startswith(expected_field) for error in errors):
            raise GateError(f"negative self-test did not fail: {case_name}")
        if marker in "\n".join(errors):
            raise GateError(f"negative self-test diagnostics were unsafe: {case_name}")


def exercise_shell_credential_lifecycle(config: Mapping[str, Any]) -> None:
    """Run the rendered initializer logic against an isolated temporary directory."""

    service = _mapping(_mapping(config.get("services")).get(INTERNAL_CREDENTIAL_INIT))
    command = service.get("command")
    if (
        not isinstance(command, list)
        or len(command) < 3
        or not isinstance(command[2], str)
    ):
        raise GateError("credential lifecycle shell command is incomplete")

    with tempfile.TemporaryDirectory(prefix="astron-credential-") as temporary:
        root = Path(temporary)
        workflow_directory = root / "workflow"
        tenant_directory = root / "tenant"
        script = command[2].replace("$$", "$")
        script = script.replace("/secrets/workflow", str(workflow_directory))
        script = script.replace("/secrets/tenant", str(tenant_directory))

        def run_initializer(
            environment: Mapping[str, str], should_succeed: bool
        ) -> None:
            completed = subprocess.run(
                ["/bin/sh", "-ec", script],
                cwd=temporary,
                env={**os.environ, **environment},
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,

View on GitHub (pinned to 5e758547a8)