RUL-004 — Incus, not Docker, for containers
Ruling
The user runs containers on Incus (LXD), which he considers more stable and powerful than Docker. docker-compose.yml is the service-topology REFERENCE only — actual deployment and test-isolation should target Incus.
Evidence
- Memory
project-incus-not-docker(stated 2026-06-20): the repo ships adocker/dir, butharness/isolation.py'sIncusIsolation.createwas previously (wrongly) flagged dead code — Incus is the preferred isolation backend, in-scope not dead. docs/LEVEL0.mdLevel-0 foundation block states the infra topology plainly: "Incus (LXD) containers on 10.53.1.x — NOT docker, NOT localhost".
Enforcement
- gate scripts/check_incus_not_docker.py RUL-004
- banned-deps docker
Verification
scripts/check_incus_not_docker.py is an introduction ratchet with four rules: (A) shell/systemd invocation of docker, (B) AST-position Python — subprocess argv[0] docker, import docker, or a Docker detection predicate such as Path("Dockerfile").exists(), (C) a Docker endpoint in config/env (DOCKER_HOST, a docker distribution in pyproject.toml), (D) introduction of a docker-compose.yml/Dockerfile outside the allowlisted docker/ and infra/docker/ reference trees. Invoked from scripts/health_check.py as "Incus not Docker (RUL-004)". docker is also a .claude/banned-libraries.txt entry, so the Docker SDK is blocked by the no-banned-deps.sh PreToolUse guard and code-review.py's BANNED-DEP gate as well.
Measured on the live tree: 2789 in-scope files scanned, 19 carrying a docker token graded, 0 violations — the gate lands green with no baseline and no suppressions. The AST/grammar rules are load-bearing, not decorative: a naive raw-text docker regex over the same scope yields 3 hits, all 3 false positives (a prose string literal in scripts/gen_level0.py, a comment and a docstring in tests/agents/test_runtime_main.py); the designed rules yield 0. Those sites are regression-locked so a future "simplify it to grep" fails loudly.
False-positive hardening, 2026-08-05 — two classes found by adversarial review, both fixed, neither losing recall:
- Rule B applied the shell grammar to every
argvelement, making a search pattern byte-indistinguishable from a command:subprocess.run(["grep", "-rn", "docker run", root])— a companion audit that enforces this very ruling — reported "runs a shell command invoking docker (run)". 327 such calls exist across 93 in-scope Python files, four of them sibling ruling gates. Fixed: argv strings are parsed as shell only when a shell will actually interpret them (os.system/os.popen/create_subprocess_shell,shell=True, orargv[0]in{sh,bash,zsh,dash,ksh,fish}). - Rule A treated a quote and
=as execution positions, sogrep -q "docker run",grep -rn 'docker run' src/,[ "$CMD" = "docker run" ],jq '... == "docker run"'and even the bare assignmentLEGACY_CMD="docker run redis"read as invocations — idioms already present in 11 of the 25 in-scope shell files. Fixed: a quote opens a command only after a shell-c/-lcflag.
Recall verified against a 13-violation battery that still fires in full: docker compose up, sudo docker run, incus exec … -- docker compose up, bash -c "docker run", sudo sh -lc 'docker compose up', subprocess.run(["docker", …]), shell=True, ["bash","-lc",…], os.system, create_subprocess_shell, systemd ExecStart, DOCKER_HOST, and a deploy/docker-compose.yml.
The allowlisted docker/ tree keeps docker-compose.yml legal as topology reference; what the gate blocks is a caller — an identical docker compose ... up -d line stays silent inside docker/up.sh and fires from scripts/boot.sh. Two frozen handover artifacts (rtm/scripts/setup_test_infrastructure.sh, rtm/scripts/check_test_infrastructure.sh) run docker run -d / docker exec ... cypher-shell and are reported on every run as a non-blocking QUARANTINE with a delete recommendation; the live equivalent is scripts/start_test_services.py against the Incus test fleet. Known bounds, stated rather than hidden: a Docker path added entirely inside an allowlisted tree with no in-scope caller is undetected; dot-directories are pruned from the walk; src/orchestrata_tools/ is exempt from the detection-predicate clause only (it analyses foreign projects for a living) and stays covered for subprocess-argv and import docker.