{"record":{"id":"ed999be39dbd4165","repo":"iflytek/astron-agent","slug":"credential-lifecycle-shell-command-is-incomplete","errorCode":null,"errorMessage":"credential lifecycle shell command is incomplete","messagePattern":"credential lifecycle shell command is incomplete","errorType":"console","errorClass":"GateError","httpStatus":null,"severity":"error","filePath":"docker/astronAgent/scripts/verify_security_contract.py","lineNumber":706,"sourceCode":"        mutate(candidate, marker)\n        errors = validate_contract(candidate)\n        if not any(error.startswith(expected_field) for error in errors):\n            raise GateError(f\"negative self-test did not fail: {case_name}\")\n        if marker in \"\\n\".join(errors):\n            raise GateError(f\"negative self-test diagnostics were unsafe: {case_name}\")\n\n\ndef exercise_shell_credential_lifecycle(config: Mapping[str, Any]) -> None:\n    \"\"\"Run the rendered initializer logic against an isolated temporary directory.\"\"\"\n\n    service = _mapping(_mapping(config.get(\"services\")).get(INTERNAL_CREDENTIAL_INIT))\n    command = service.get(\"command\")\n    if (\n        not isinstance(command, list)\n        or len(command) < 3\n        or not isinstance(command[2], str)\n    ):\n        raise GateError(\"credential lifecycle shell command is incomplete\")\n\n    with tempfile.TemporaryDirectory(prefix=\"astron-credential-\") as temporary:\n        root = Path(temporary)\n        workflow_directory = root / \"workflow\"\n        tenant_directory = root / \"tenant\"\n        script = command[2].replace(\"$$\", \"$\")\n        script = script.replace(\"/secrets/workflow\", str(workflow_directory))\n        script = script.replace(\"/secrets/tenant\", str(tenant_directory))\n\n        def run_initializer(\n            environment: Mapping[str, str], should_succeed: bool\n        ) -> None:\n            completed = subprocess.run(\n                [\"/bin/sh\", \"-ec\", script],\n                cwd=temporary,\n                env={**os.environ, **environment},\n                stdout=subprocess.PIPE,\n                stderr=subprocess.PIPE,","sourceCodeStart":688,"sourceCodeEnd":724,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/docker/astronAgent/scripts/verify_security_contract.py#L688-L724","documentation":"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 $$.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"# before\ncommand: /entrypoint.sh\n# after\ncommand:\n  - bash\n  - -c\n  - |\n    export TENANT_DB_PASSWORD=$$TENANT_DB_PASSWORD\n    ...","handlingStrategy":"validation","validationCode":"import json, subprocess\ncfg = json.loads(subprocess.run([\"docker\",\"compose\",\"-f\",f,\"config\",\"--format\",\"json\"], capture_output=True, text=True).stdout)\ncmd = cfg[\"services\"][\"workflow\"].get(\"command\")\nassert isinstance(cmd, list) and len(cmd) >= 3 and isinstance(cmd[2], str), f\"bad command: {cmd!r}\"","typeGuard":"def has_shell_command(command) -> bool:\n    return (isinstance(command, list) and len(command) >= 3\n            and isinstance(command[2], str))","tryCatchPattern":"try:\n    exercise_shell_credential_lifecycle(cfg)\nexcept GateError as e:\n    if \"shell command is incomplete\" in str(e):\n        print(json.dumps(service.get(\"command\")))  # inspect actual shape\n    raise","preventionTips":["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."],"tags":["docker-compose","yaml","schema-validation","scripting"],"backgroundTag":"schema-validation-failed","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}