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
+63 -6
View File
@@ -25,7 +25,7 @@ apiVersion: ax.io/v1alpha1
kind: Task
metadata:
name: verify
namespace: sast
atespace: sast
spec:
image: openultrasast:dev
command: ["verify", "--pass", "a"]
@@ -47,12 +47,15 @@ apiVersion: ax.io/v1alpha1
kind: Workspace
metadata:
name: flask-abc123
annotations:
openultrasast.io/git-commits: "repo=0123456789abcdef0123456789abcdef01234567"
spec:
git:
- name: repo
repo: https://github.com/pallets/flask.git
branch: main
commit: abc123
dir: flask
depth: 1
files:
- path: candidates.json
content: '{"sinks": []}'
@@ -143,7 +146,7 @@ def test_task_loads_every_ax_field() -> None:
task = parse_manifest(doc(TASK_YAML))
assert isinstance(task, Task)
assert task.metadata.name == "verify"
assert task.metadata.namespace == "sast"
assert task.metadata.atespace == "sast"
assert task.image == "openultrasast:dev"
assert task.command == ("verify", "--pass", "a")
assert task.env[0].name == "OUSAST_PASS" and task.env[0].value == "a"
@@ -162,12 +165,66 @@ def test_task_minimal_defaults() -> None:
def test_workspace_loads() -> None:
workspace = parse_manifest(doc(WORKSPACE_YAML))
assert isinstance(workspace, Workspace)
assert workspace.git[0].repo.endswith("flask.git") and workspace.git[0].commit == "abc123"
assert workspace.git[0].repo.endswith("flask.git")
assert workspace.git[0].dir == "flask" and workspace.git[0].depth == 1
assert workspace.pins == {"repo": "0123456789abcdef0123456789abcdef01234567"}
assert workspace.files[0].path == "candidates.json"
assert workspace.mcp is not None and len(workspace.mcp.registries) == 1 and len(workspace.mcp.servers) == 1
assert workspace.skills is not None and workspace.skills.path == "/skills"
SMOKE = Path(__file__).parent / "fixtures" / "plane" / "smoke-task.yaml"
def test_the_bring_up_smoke_manifest_loads() -> None:
"""``ops/ax/smoke-task.yaml`` (copied as a fixture) is what ``ax apply`` accepted; it must load here too."""
manifests = load_manifests([SMOKE])
assert manifests.tasks["ousast-smoke"].metadata.atespace == "default"
assert manifests.workspaces["ousast-smoke-ws"].metadata.atespace == "default"
def test_the_shipped_smoke_manifest_matches_the_fixture() -> None:
shipped = Path(__file__).parent.parent / "ops" / "ax" / "smoke-task.yaml"
if not shipped.is_file():
pytest.skip("ops/ax/smoke-task.yaml is not in this checkout")
assert load_manifests([shipped]).tasks.keys() == {"ousast-smoke"}
@pytest.mark.parametrize("key", ["namespace", "labels", "creation_timestamp"])
def test_metadata_takes_ax_names_only(key: str) -> None:
task = doc(TASK_YAML)
task["metadata"][key] = "sast"
rejected(task, f"Task/verify: metadata.{key} is not a field")
def test_git_commit_is_rejected_with_a_pointer_to_the_annotation() -> None:
workspace = doc(WORKSPACE_YAML)
workspace["spec"]["git"][0]["commit"] = "abc123"
message = rejected(workspace, "Workspace/flask-abc123: spec.git[0].commit is not an ax field")
assert "openultrasast.io/git-commits" in message
@pytest.mark.parametrize(
("value", "rule"),
[
("other=" + "a" * 40, '"other" names no spec.git entry'),
("repo=abc123", '"repo=abc123" is not a 40-hex commit'),
("repo", 'item "repo" must be'),
],
)
def test_git_commits_annotation_is_validated(value: str, rule: str) -> None:
workspace = doc(WORKSPACE_YAML)
workspace["metadata"]["annotations"]["openultrasast.io/git-commits"] = value
message = rejected(workspace, 'metadata.annotations["openultrasast.io/git-commits"]')
assert rule in message, message
def test_git_depth_must_be_an_integer() -> None:
workspace = doc(WORKSPACE_YAML)
workspace["spec"]["git"][0]["depth"] = "1"
rejected(workspace, "Workspace/flask-abc123: spec.git[0].depth must be a non-negative integer")
def test_model_loads_with_declared_extension() -> None:
model = parse_manifest(doc(MODEL_YAML))
assert isinstance(model, Model)
@@ -235,8 +292,8 @@ def test_unknown_nested_fields_are_named_with_index() -> None:
task["spec"]["resources"]["requests"]["gpu"] = "1"
rejected(task, "spec.resources.requests.gpu is not a field")
workspace = doc(WORKSPACE_YAML)
workspace["spec"]["git"][0]["depth"] = 1
rejected(workspace, "Workspace/flask-abc123: spec.git[0].depth is not a field")
workspace["spec"]["git"][0]["submodules"] = True
rejected(workspace, "Workspace/flask-abc123: spec.git[0].submodules is not a field")
model = doc(MODEL_YAML)
model["spec"]["secretKey"]["namespace"] = "x"
rejected(model, "Model/deepseek-flash: spec.secretKey.namespace is not a field")