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
- Restore the service command in the compose file to a 3-element list form: ["bash", "-c", "<script with $$-escaped $>"] and re-run the gate.
- Validate locally with 'docker compose config --format json | jq ".services.<name>.command"' to see the parsed type/shape.
- Quote the script argument in YAML (block scalar |) so YAML does not coerce it to a non-string.
- 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
- Define service commands as YAML block-scalar lists with $$-escaped $.
- Snapshot-test the rendered compose command shape.
- Review compose command changes against the security-gate assumptions.
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)