feat(plane): runner contract with artifact delivery (task 3)

ai-service-plane task 3, Requirements 4.1-4.4 (with 2.2 and 2.3 honoured by the
exit-code contract):

- src/openultrasast/plane/runner.py: the ax-task-runner entrypoint. Reads the Task
  from AX_TASK_YAML and the bound Workspaces from AX_WORKSPACES_YAML (task-1 loaders),
  serves /healthz and /readyz on port 80 (AX_RUNNER_PORT; 0 picks a free port,
  AX_RUNNER_HTTP=0 disables it for unit tests), materialises each workspace at its
  bound path (spec.files verbatim; spec.git at <path>/<name>, exported from the
  OUSAST_CASE_CACHE checkout whose origin matches, else a shallow clone at the pin;
  a repo already present on a resumed volume is kept) and only then answers 200 on
  /readyz. Runs spec.command by importing openultrasast.plane.tasks.<name> with the
  remaining elements as argv; sets OUSAST_WORKSPACE_DIR from the first binding when
  the Task's env leaves it unset. A crash writes summary.json with status failed and
  the traceback. When OUSAST_ARTIFACT_URL is set, the whole OUSAST_OUTPUT_DIR is
  POSTed as an uncompressed tar (Content-Type application/x-tar, X-Ousast-Task,
  X-Ousast-Run) with three attempts and backoff; a failed delivery rewrites
  summary.json to failed and exits 2. Exit codes: done 0, failed 2, unfinished 3.
- plane/Dockerfile.runner: python:3.12-slim + git, `pip install .` (no extras), a
  shell wrapper at /usr/local/bin/ax-task-runner exec'ing the module; built and
  loaded into kind by ops/ax/up.sh, not by the tests.
- tests/test_plane_runner.py (+ tests/plane_stub_task.py registered as the task
  module): env contract on a files workspace, readyz 503 then 200, delivery to a
  local receiver with the headers and tar checked, delivery failure -> failed and
  exit 2 after three attempts, crash -> failed with traceback, exit codes per
  status, case-cache export at the pin, shallow-clone fallback, resume keeps the
  checkout.
- model/audit.py: SPEC_OWNED row for plane.runner; module-audit measurement
  regenerated (122 modules, no orphans, note kept).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-29 18:11:24 +02:00
co-authored by Claude Fable 5.1
parent 488e63951e
commit 86e7a61f83
6 changed files with 926 additions and 3 deletions
+32
View File
@@ -0,0 +1,32 @@
# The ax task-runner image for this project (ai-service-plane Requirement 4.2).
#
# ax's default runner image is gcr.io/ax-substrate/ate-images/ax-task-runner@sha256:3a0dea6a... (Python 3.12,
# git, curl, ax's Go runner as PID 1). This image keeps the same base and the same contract -- PID 1 is
# /usr/local/bin/ax-task-runner, fed AX_TASK_YAML / AX_WORKSPACES_YAML, serving /healthz and /readyz on port 80 --
# but the runner is this package's `openultrasast.plane.runner`, so a task's spec.command names a module under
# `openultrasast.plane.tasks` and the output directory is delivered to the reconciler at exit.
#
# Build from the repository root (the package is the context):
# docker build -f plane/Dockerfile.runner -t openultrasast-runner:dev .
# `ops/ax/up.sh` builds it and loads it into kind; nothing here is exercised by the unit tests.
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/openultrasast
COPY pyproject.toml README.md ./
COPY src ./src
# Core install only: PyYAML for the manifests, no extras (no HarnessX, no tree-sitter).
RUN pip install --no-cache-dir . \
&& printf '#!/bin/sh\nexec python -m openultrasast.plane.runner "$@"\n' > /usr/local/bin/ax-task-runner \
&& chmod 0755 /usr/local/bin/ax-task-runner
# ax mounts the persistent volume here; it is also the command's working directory in ax's own runner.
RUN mkdir -p /workspace
WORKDIR /workspace
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/ax-task-runner"]