Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/developer/codex-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 48 additions & 10 deletions docs/developer/qnap-release-deployment-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<qnap-user>@<qnap-host>` 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <command>; 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
Expand Down
7 changes: 7 additions & 0 deletions docs/developer/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_qnap_docker_runtime_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading