fix(plane): ax field names verbatim; pins and bindings as stripped annotations

Requirement 1.1 (Task/Workspace/Model use only fields ax documents) and
Requirement 1.4 (no ax field repurposed) were violated: manifests accepted
metadata.namespace and a Workspace git `commit`, and the reconciler dumped
every git field into what `ax apply` receives.

Facts from ax's source (pkg/apis/v1alpha1/ax.proto, internal/workspace/setup.go):
- ObjectMeta is `name` and `atespace` (plus server-set creation_timestamp);
  no `namespace`, no `annotations`.
- GitRepo is `name`, `repo`, `branch`, `dir`, `depth`; no `commit`.
- The API server decodes with protojson.UnmarshalOptions{}: an unknown field
  is an error at `ax apply` (confirmed on the live cluster: a rendered
  Workspace+Model applied cleanly; the same Workspace with `commit` was
  refused with `unknown field "commit"`).
- ValidateModel checks only metadata; any spec.provider string is accepted.

Changes:
- manifests: metadata takes `name`, `atespace`, `annotations` (our extension,
  never sent to ax); git entries take exactly ax's five fields (depth int);
  `commit` in spec.git is rejected with a pointer to the new Workspace
  annotation `openultrasast.io/git-commits: "<git name>=<40-hex>,..."`,
  validated for unknown git names and non-hex shas.
- reconciler: rendered docs carry only ax fields; each bound Workspace's pins
  reach the Task as env OUSAST_GIT_PINS = {"<workspace>/<git name>": "<sha>"}.
  A test walks every rendered doc against ax's field lists.
- runner: pins come from OUSAST_GIT_PINS; git is fetched as setup.go does
  (init/remote/fetch with --depth when depth > 0/checkout FETCH_HEAD, default
  branch main, cloneDestination's dir and placeholder-name rules), then the
  pin is checked out. A pinned entry without a branch fetches the commit.
- workspaces: emits the annotation, `dir: repo` and `depth: 1` instead of
  `commit`, so generated case workspaces keep <path>/repo at depth 1.
- design.md: ax field names verbatim; pins and model binding as stripped
  openultrasast.io/* annotations carried to the task by env.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-29 18:36:29 +02:00
co-authored by Claude Opus 5.5
parent e6d5124ffc
commit 18b67cf942
9 changed files with 450 additions and 81 deletions
+6 -3
View File
@@ -1,8 +1,9 @@
"""Workspace manifests for the cases of an independent population file (ai-service-plane task 4).
``ousast plane workspaces <population.toml> [--out DIR]`` writes one ax ``Workspace`` per case pin (vulnerable,
fixed, benign base, benign tip) with the case's repository URL and the pinned commit, so a Run binds a case at a
pin by name. The layout of a case is that of ``benchmarks/independent/evaluate.py``'s ``pins()``.
fixed, benign base, benign tip) with the case's repository URL (checked out at ``<path>/repo``, depth 1) and the
pinned commit in the ``openultrasast.io/git-commits`` annotation (ax's ``GitRepo`` has no commit field), so a Run
binds a case at a pin by name. The layout of a case is that of ``benchmarks/independent/evaluate.py``'s ``pins()``.
"""
from __future__ import annotations
@@ -12,6 +13,7 @@ from pathlib import Path
import yaml
from .manifests import GIT_COMMITS_ANNOTATION
from .reconciler import _ax_name, _doc
__all__ = ["workspaces"]
@@ -26,7 +28,8 @@ def workspaces(population: Path, out: Path) -> list[Path]:
pins = {"vulnerable": case["vulnerable"], "fixed": case["fixed"], "benign-base": benign["base"], "benign-tip": benign["tip"]}
for label, commit in pins.items():
name = _ax_name(case["id"], label)
doc = _doc("Workspace", name, {"git": [{"name": "repo", "repo": case["repo"], "commit": commit}]})
doc = _doc("Workspace", name, {"git": [{"name": "repo", "repo": case["repo"], "dir": "repo", "depth": 1}]})
doc["metadata"]["annotations"] = {GIT_COMMITS_ANNOTATION: f"repo={commit}"}
text = f"# generated by `ousast plane workspaces` from {population.name}\n" + yaml.safe_dump(doc, sort_keys=False)
(out / f"{name}.yaml").write_text(text, encoding="utf-8")
written.append(out / f"{name}.yaml")