diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5f41c18 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.pytest_cache +.mypy_cache +.ruff_cache +.venv +__pycache__ +*.pyc +outputs +worktrees +tests +docs +candidate-manifests diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6f79d78 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + SVRNTY_VISION_HOST=0.0.0.0 \ + SVRNTY_VISION_PORT=8094 + +WORKDIR /app + +RUN useradd --create-home --shell /usr/sbin/nologin vision + +COPY pyproject.toml README.md ./ +COPY src ./src + +RUN python -m pip install --no-cache-dir --upgrade pip \ + && python -m pip install --no-cache-dir . + +USER vision + +EXPOSE 8094 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8094/healthz', timeout=3).read()" + +CMD ["python", "-m", "svrnty_vision.server"] diff --git a/WORKBOARD.yaml b/WORKBOARD.yaml index cf7692c..b8d6c09 100644 --- a/WORKBOARD.yaml +++ b/WORKBOARD.yaml @@ -14,3 +14,8 @@ items: status: ready-for-agent source: outputs/sandcastle-vision-package-candidate/README.md owner: jp + - id: SVRNTY-VISION-WORK-004 + title: Package Docker Build Context + status: complete + source: Dockerfile + owner: jp diff --git a/tools/validate_svrnty_vision_child.py b/tools/validate_svrnty_vision_child.py index 9702d74..62c201e 100755 --- a/tools/validate_svrnty_vision_child.py +++ b/tools/validate_svrnty_vision_child.py @@ -19,12 +19,16 @@ REQUIRED = [ "candidate-manifests/vision-package-candidate.json", "candidate-manifests/vision-tool-grants.json", "candidate-manifests/visual-evidence-contract.json", + "Dockerfile", + ".dockerignore", + "tools/validate_vision_package_docker_context.py", ] VALIDATORS = [ "tools/validate_vision_package_candidate.py", "tools/validate_vision_tool_grants.py", "tools/validate_visual_evidence_contract.py", "tools/validate_vision_host_adapter_candidates.py", + "tools/validate_vision_package_docker_context.py", ] diff --git a/tools/validate_vision_package_docker_context.py b/tools/validate_vision_package_docker_context.py new file mode 100644 index 0000000..edb8900 --- /dev/null +++ b/tools/validate_vision_package_docker_context.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Validate the no-live Vision package Docker build context.""" +from __future__ import annotations + +import json +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DOCKERFILE = ROOT / "Dockerfile" +DOCKERIGNORE = ROOT / ".dockerignore" +WORKBOARD = ROOT / "WORKBOARD.yaml" + +REQUIRED_DOCKER_SNIPPETS = [ + "FROM python:3.12-slim", + "SVRNTY_VISION_HOST=0.0.0.0", + "SVRNTY_VISION_PORT=8094", + "COPY pyproject.toml README.md ./", + "COPY src ./src", + "python -m pip install --no-cache-dir .", + "USER vision", + "EXPOSE 8094", + "HEALTHCHECK", + "http://127.0.0.1:8094/healthz", + 'CMD ["python", "-m", "svrnty_vision.server"]', +] +REQUIRED_IGNORE = { + ".git", + ".venv", + "__pycache__", + "outputs", + "worktrees", + "tests", + "candidate-manifests", +} +FORBIDDEN_PATTERNS = [ + re.compile(r"sk-[A-Za-z0-9_-]{20,}"), + re.compile(r"AIza[0-9A-Za-z_-]{20,}"), + re.compile(r"\b(?:ck|cs)_[0-9A-Za-z]{20,}"), + re.compile(r"(?i)\b[A-Z0-9_]*(?:PASS|PASSWORD|SECRET|TOKEN|KEY)[A-Z0-9_]*\s*=\s*[^`\s#]{8,}"), +] + + +def main() -> int: + errors: list[str] = [] + dockerfile = DOCKERFILE.read_text(encoding="utf-8") if DOCKERFILE.is_file() else "" + dockerignore = DOCKERIGNORE.read_text(encoding="utf-8") if DOCKERIGNORE.is_file() else "" + workboard = WORKBOARD.read_text(encoding="utf-8") if WORKBOARD.is_file() else "" + if not dockerfile: + errors.append("missing:Dockerfile") + if not dockerignore: + errors.append("missing:.dockerignore") + for snippet in REQUIRED_DOCKER_SNIPPETS: + if snippet not in dockerfile: + errors.append(f"dockerfile_missing:{snippet}") + ignore_rows = {line.strip() for line in dockerignore.splitlines() if line.strip() and not line.startswith("#")} + for row in sorted(REQUIRED_IGNORE - ignore_rows): + errors.append(f"dockerignore_missing:{row}") + if "SVRNTY-VISION-WORK-004" not in workboard: + errors.append("workboard_missing:SVRNTY-VISION-WORK-004") + if "Package Docker Build Context" not in workboard: + errors.append("workboard_missing:Package Docker Build Context") + combined = "\n".join([dockerfile, dockerignore, workboard]) + for pattern in FORBIDDEN_PATTERNS: + if pattern.search(combined): + errors.append(f"value_pattern_found:{pattern.pattern}") + result = { + "ok": not errors, + "validator": "vision-package-docker-context-no-live", + "checked": ["Dockerfile", ".dockerignore", "WORKBOARD.yaml"], + "errors": errors, + "warnings": [], + } + print(json.dumps(result, indent=2)) + return 0 if result["ok"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main())