diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a0ac418..ed55511 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -215,4 +215,4 @@ The repository identity check must treat organization acronyms as standalone ter ## QNAP release deployment authority -For QNAP deployment, verification, hosted acceptance, or rollback, follow the authoritative [QNAP Release Deployment Runbook](docs/developer/qnap-release-deployment-runbook.md). Do not invent or substitute another deployment procedure. +For QNAP deployment, verification, hosted acceptance, or rollback, follow the authoritative [QNAP Release Deployment Runbook](docs/developer/qnap-release-deployment-runbook.md). Do not invent or substitute another deployment procedure. Generated QNAP command text must use that runbook's exact connection-identity, sudo-preflight, BusyBox, one-line, PASS/FAIL, and standalone-interactive-SSH safeguards. diff --git a/AGENTS.md b/AGENTS.md index e186492..d6b7555 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,6 +142,9 @@ later phase; RL-MERGE, deployment, and issue closure need explicit authority. a clean local archive, calculate its hash, generate local archive-transfer or runbook-derived QNAP command text, prepare hosted-acceptance checklists, and interpret safe output pasted by the user. +- Generated QNAP command text must follow the authoritative runbook's + connection-identity, sudo-preflight, BusyBox, one-line, PASS/FAIL, and + standalone-interactive-SSH safeguards; do not create a competing procedure. - Keep QNAP-specific host paths and credentials out of application code. Use docs, placeholders, environment variables, or operator notes instead. ## Change behavior diff --git a/docs/developer/codex-workflow.md b/docs/developer/codex-workflow.md index abe8a6f..5a6bf80 100644 --- a/docs/developer/codex-workflow.md +++ b/docs/developer/codex-workflow.md @@ -443,6 +443,11 @@ roll back, restore PostgreSQL, or administer Cloudflare. The user alone performs archive-transfer and QNAP commands through the approved local transfer workflow and standalone SSH client. +When a task authorizes generating command text for that human-operated path, +derive it only from the authoritative QNAP release deployment runbook, including +its exact connection-identity, sudo-preflight, BusyBox, one-line, PASS/FAIL, and +standalone-interactive-SSH safeguards. This does not authorize QNAP access. + ## Project Sources Repository `main` is authoritative. ChatGPT Project Sources are static diff --git a/docs/developer/qnap-release-deployment-runbook.md b/docs/developer/qnap-release-deployment-runbook.md index dbc4a5a..1e3c9ad 100644 --- a/docs/developer/qnap-release-deployment-runbook.md +++ b/docs/developer/qnap-release-deployment-runbook.md @@ -52,6 +52,29 @@ repository. The user may use a private PSD1 file, password manager, operator notes, or manual substitution. No agent may read, create, display, upload, or depend on that private store, and no agent may create a private PSD1 file. +## Operator connection and privilege preflight + +When the operator supplies an exact `@` value, generated +local `scp` and connection instructions must use that exact value verbatim. Do +not silently substitute an SSH alias; use an alias only when the operator +explicitly requests it. Committed examples continue to use placeholders and +must not contain a private username, host, IP address, key path, or alias. + +`scp` runs locally without `sudo`; its remote destination must be writable by +the operator account before transfer. On QNAP, use `sudo` for Docker and Compose +commands and for protected deployment paths, configuration preservation, +backups, directory activation, and `.deployed-commit` marker writes. Before the +first QNAP mutation, the operator must separately verify both required sudo +access and Docker Compose availability. A generated one-line gate may use this +BusyBox-compatible shape: + +```sh +if sudo -v && sudo docker compose version; then printf '%s\n' PASS; else printf '%s\n' FAIL; false; fi +``` + +Do not invent extra host-tool prerequisites: verify only the prerequisites that +the documented procedure actually requires. + ## Required deployment model 1. Verify clean local `main` and `origin/main` at the exact approved merged commit. @@ -205,16 +228,16 @@ from `/share/Public/RecordsTracker`. Agents must not run or proxy these commands 1. Generate the read-only structural audit and atomically publish the validated contract package: - ```sh - sudo docker compose -f docker-compose.qnap.yml exec -T app python -m ccld_complaints.source_to_screen_audit --mode runtime --output-dir /app/data/processed/source-to-screen-audit/runtime-audit --coverage-output-dir /app/data/processed/source-to-screen-audit/runtime-current - ``` + ```sh + if sudo docker compose -f docker-compose.qnap.yml exec -T app python -m ccld_complaints.source_to_screen_audit --mode runtime --output-dir /app/data/processed/source-to-screen-audit/runtime-audit --coverage-output-dir /app/data/processed/source-to-screen-audit/runtime-current; then printf '%s\n' PASS; else printf '%s\n' FAIL; false; fi + ``` 2. Revalidate package hashes and reconcile its Facility ID totals with a safe deployed-database aggregate query: - ```sh - sudo docker compose -f docker-compose.qnap.yml exec -T app python -m ccld_complaints.operator_coverage_runtime_verify --package-dir /app/data/processed/source-to-screen-audit/runtime-current - ``` + ```sh + if sudo docker compose -f docker-compose.qnap.yml exec -T app python -m ccld_complaints.operator_coverage_runtime_verify --package-dir /app/data/processed/source-to-screen-audit/runtime-current; then printf '%s\n' PASS; else printf '%s\n' FAIL; false; fi + ``` Both commands are read-only with respect to PostgreSQL. The producer publishes files only after complete stable-consumer validation and preserves the prior @@ -327,10 +350,25 @@ reviewer-state mutation, snapshot deletion, deployment, or Cloudflare operation. ## Operator command standards -- QNAP commands must be BusyBox-compatible unless a required utility was explicitly verified. -- Use one complete copy/pasteable one-line command per numbered operator step. -- Commands must fail nonzero on verification failure. -- Print PASS only after the operation and its verification succeed. +- QNAP commands must be BusyBox-compatible unless this runbook requires and the + operator has verified a utility. +- Use one complete copy/pasteable one-line command per numbered operator step; + it must be safe when pasted individually into an existing standalone SSH + session. +- Do not generate `set -e`, `set -u`, `set -eu`, `exit`, heredoc execution, + Bash-only syntax, or any command intended to terminate the interactive SSH + session. +- When branching is required, use BusyBox-compatible `if ...; then ...; else + ...; fi` or an equivalent safe form. Do not rely on shell options or `exit` + for control flow. +- Every numbered mutating command or decision gate must explicitly print `PASS` + only after its operation and verification succeed, or explicitly print `FAIL` + on failure, and return nonzero on failure without terminating the SSH session. + Informational output explicitly intended for operator inspection does not need + meaningless PASS/FAIL decoration. +- A safe generated gate pattern is `if ; then printf '%s\n' PASS; else + printf '%s\n' FAIL; false; fi`; substitute the documented command and retain + any required verification before the PASS result. - Keep Windows PowerShell commands separate from standalone QNAP SSH commands. ## Maintenance diff --git a/docs/developer/testing.md b/docs/developer/testing.md index 79ea4a9..74f8360 100644 --- a/docs/developer/testing.md +++ b/docs/developer/testing.md @@ -491,6 +491,13 @@ validation, optional container/PostgreSQL/Alembic checks, route probes, no-secre markers, configurable raw storage, mock-success demo guardrails, and absence of hard-coded QNAP-only app paths. +QNAP release-runbook documentation tests should protect the authoritative +operator-command contract: forbidden interactive-shell controls, exact +operator-supplied connection identity, required sudo and Docker Compose +preflight, BusyBox-compatible one-line standalone-SSH commands, and explicit +PASS/FAIL results for numbered mutations and decision gates. They should retain +the separately governed exceptional PostgreSQL backup-recovery safeguards. + Hosted reviewer-created state read route tests must prove persisted scaffold rows are readable only through authenticated, active, role/scope-allowed local/ test routes with reviewer-state read permission. Tests should cover authorized diff --git a/tests/unit/test_qnap_docker_runtime_config.py b/tests/unit/test_qnap_docker_runtime_config.py index 1226c8f..e1c0462 100644 --- a/tests/unit/test_qnap_docker_runtime_config.py +++ b/tests/unit/test_qnap_docker_runtime_config.py @@ -318,6 +318,36 @@ def test_release_runbook_preserves_reviewed_compose_and_application_only_rollbac assert "docker compose -f docker-compose.qnap.yml exec app python" not in runbook +def test_release_runbook_enforces_safe_operator_shell_command_generation() -> None: + runbook = read_repo_text("docs/developer/qnap-release-deployment-runbook.md") + normalized_runbook = " ".join(runbook.split()) + + for required_text in ( + "must use that exact value verbatim", + "Do not silently substitute an SSH alias", + "`scp` runs locally without `sudo`", + "destination must be writable by the operator account", + "required sudo access and Docker Compose availability", + "sudo -v && sudo docker compose version", + "BusyBox-compatible", + "one complete copy/pasteable one-line command", + "existing standalone SSH session", + "set -e", + "set -u", + "set -eu", + "heredoc execution", + "Bash-only syntax", + "terminate the interactive SSH session", + "if ...; then ...; else ...; fi", + "explicitly print `PASS`", + "explicitly print `FAIL`", + "return nonzero on failure without terminating the SSH session", + "protected deployment paths, configuration preservation, backups, " + "directory activation, and `.deployed-commit` marker writes", + ): + assert required_text in normalized_runbook + + def test_release_runbook_limits_exceptional_pre_backup_recovery() -> None: runbook = read_repo_text("docs/developer/qnap-release-deployment-runbook.md") normalized_runbook = " ".join(runbook.split())