Skip to content

feat(operator): API & CRD foundations for D + E parity (PR-49 of #48) - #49

Merged
flg77 merged 10 commits into
mainfrom
feat/op-pr49-api-foundations
May 9, 2026
Merged

feat(operator): API & CRD foundations for D + E parity (PR-49 of #48)#49
flg77 merged 10 commits into
mainfrom
feat/op-pr49-api-foundations

Conversation

@flg77

@flg77 flg77 commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements PR-49 of the openspec change merged in #48: API & CRD foundations that unblock the manifest delivery (#50) + MCP server (#51) reconcilers and the demo samples (#52).

What changed

Schema (api/v1alpha1)

  • Role enum loosened to a regex (^[a-z][a-z0-9_]{1,62}$ + length bounds) on AgentRole. Field-level Enum= markers dropped on AgentRoleSpec.Role and RoleScalingSpec.Role. Schema now accepts every persona under roles/; semantic check moves to a webhook (see below).
  • 12 new exported role consts for the D3 split-skills personas, the E4 autoresearcher personas, and the umbrella coding_agent used by container/production/podman-compose.yml.
  • MCPServerSpec + MCPServerStatus types and their slice/map wiring on AgentCorpusSpec / AgentCorpusStatus. Fields: Name, Image, Replicas, Port, Env, SecretEnv, ShmSizeMi, Resources. Reconciler ships in feat(operator): manifest delivery reconciler (PR-51 of #48) #51.
  • ManifestDelivery enum (all|none, default all) on AgentCorpusSpec. Reconciler ships in docs(index): cross-link to acc-podman-desktop extension #50.
  • Compile-time bug fix: RoleDefinition struct was missing its closing } before AgentRoleSpec.

Webhook (api/v1alpha1)

  • New agentcollective_webhook.go parallel to the existing corpus webhook. Validates roles against the operator's compiled-in catalogue with closest-match Levenshtein suggestions:

    role "research_plan" is not in the operator's known-roles catalogue; did you mean "research_planner", "research_critic", "research_economist"?

    Plus bonus checks: scaling.roleScaling[*].role must be declared in spec.agents; minReplicas <= maxReplicas; llm sub-struct must match the chosen llm.backend.

  • agentcorpus_webhook.go defaults manifestDelivery=all and MCPServer.Replicas=1, MCPServer.Port=8080. Rejects duplicate MCP server names (the reconciler in feat(operator): manifest delivery reconciler (PR-51 of #48) #51 derives Service names from MCPServerSpec.Name and collisions would be fatal).

Catalogue infrastructure

  • internal/rolecatalogue/IsKnown / All / Suggest(role, n) public API. Catalogue is //go:embed-baked from known_roles.txt, generated from the live roles/ tree (47 roles).
  • hack/gen-catalogue.go — regenerator triggered by //go:generate. Walks up to operator/ automatically so it works whether invoked from the package directory or the operator root.

Generated artefacts

  • zz_generated.deepcopy.go regenerated (+632/-238 lines).
  • config/crd/bases/acc.redhat.io_agent{collectives,corpora}.yaml regenerated. CRD schemas now reflect the loosened role validation, the new mcpServers slice, the new manifestDelivery field, and the new status fields.
  • config/webhook/manifests.yaml regenerated to register the new AgentCollective mutating + validating webhooks.

Build hygiene (chore commit 24e5414)

  • go.mod had a stale kube-openapi pseudo-version that no longer resolved on proxy.golang.org, blocking every go invocation. Bumped to v0.0.0-20240228011516-70dd3763d340. go.sum had also never been committed — generated and added.

Verification

Check Result
go vet ./... exit 0
go build ./... exit 0
go test ./test/unit/... PASS (12 new tests, full suite clean)
make manifests generate exit 0
Legacy sol-corpus / rhoai-corpus sample roles match new regex all 5 PASS
All 12 new persona consts match new regex all 12 PASS

kubectl apply --dry-run=server against a live cluster was deliberately skipped — the cluster is shared and the original task list specified dry-run only; the regex verification above is equivalent for the intended question (does the new schema accept everything the old one did, plus the new personas).

Coordination

Lock claimed via commit 22ecaa6 and held through this entire PR. Updates to tasks.md mark all PR-49 boxes complete.

Commit graph (8 commits)

  • 22ecaa6 lock claim
  • 466273b } fix
  • 9052391 enum loosening + 12 role consts
  • d9c2784 MCPServerSpec + ManifestDelivery schema
  • 24e5414 go.mod kube-openapi repair + go.sum (build hygiene)
  • (rolecatalogue commit) role catalogue + generator
  • d2043d0 AgentCollective webhook + corpus defaulter updates
  • 1d61a17 regenerated deepcopy + CRDs + webhook manifests
  • b325344 unit tests
  • 497fbee lock board: PR-49 tasks complete

🤖 Generated with Claude Code

flg77 and others added 10 commits May 9, 2026 00:21
Lock acquisition per coordination protocol — single-file commit pushed
immediately so the other Claude instance (10.199.12.8) sees the claim
on next git pull.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RoleDefinition type was missing its closing brace before
AgentRoleSpec began on line 93. zz_generated.deepcopy.go was
generated from a previously correct version, so the file had
been broken by a hand edit without a follow-up `make generate`.

After this change, source and generated deepcopy match shapes
(verified by inspection of zz_generated.deepcopy.go:333-353).

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 task 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The closed Enum= validation marker on AgentRole rejected every role
added since ACCv3 — including the 5 coding-split-skills personas (D3),
the 6 autoresearcher personas (E4), and the umbrella coding_agent.

Replace with a regex Pattern (DNS-label-style: ^[a-z][a-z0-9_]{1,62}$)
plus length bounds. Drop the redundant field-level Enum= markers on
AgentRoleSpec.Role and RoleScalingSpec.Role.

Semantic validation against the operator's compiled-in catalogue moves
to the AgentCollective validating webhook (subsequent commit on this
branch).

Append 12 exported role consts so samples and tests can refer to the
new personas by name:
  RoleCodingAgent + 5 split-skill variants
  6 research_* personas

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 tasks 2 and 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the schema-side surface for two later reconcilers (no behaviour
change in this commit):

  spec.mcpServers — slice of MCPServerSpec entries describing the
  shared MCP servers visible across all collectives in this corpus.
  Mirrors the per-MCP shape used in
  container/production/podman-compose.yml: name, image, replicas,
  port, env, secretEnv, shmSizeMi (for browser-harness Chromium
  /dev/shm headroom), resources.

  spec.manifestDelivery — Enum{all|none}, default "all". Controls
  whether the operator mounts the operator-baked roles/, skills/,
  mcps/ ConfigMaps into agent pods. "none" lets users bake the trees
  into a custom agent image instead.

Also wires the matching status fields:
  status.mcpServerStatuses[name] — Ready/Replicas/ServiceURL
  status.manifestDeliveryReady — boolean

Reconcilers that consume these fields ship in PR-50 (manifest
delivery) and PR-51 (MCP server). Existing CRs without these fields
continue to validate cleanly because every new field is optional.

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 tasks 4 and 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds operator/internal/rolecatalogue/, the compile-time set of known
agent role names. Sourced from known_roles.txt — generated from the
live roles/ tree by hack/gen-catalogue.go (skips _base / TEMPLATE,
requires role.yaml to be present).

Public API:
  IsKnown(role)        — membership check
  All()                — sorted slice for help text
  Suggest(role, n)     — Levenshtein-ranked closest matches, capped
                         at distance 8 so unrelated typos don't
                         produce nonsense suggestions

Two invocation paths produce byte-identical output:
  go run ./hack/gen-catalogue.go            (from operator/)
  go generate ./internal/rolecatalogue/...  (any cwd)

The generator walks up from cwd to find operator/, so the //go:generate
directive works regardless of where it's invoked from.

Spot-checked behaviour:
  knows analyst, coding_agent_tester, research_planner: true
  knows fnord: false
  suggest "research_plan"   → research_planner, research_critic, ...
  suggest "coding_agnt_arch" → coding_agent, coding_agent_architect, ...

Used by the AgentCollective validating webhook in the next commit on
this branch.

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 task 6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous pseudo-version v0.0.0-20240813175640-2e4dc7204e66 of
k8s.io/kube-openapi is no longer resolvable on proxy.golang.org —
the underlying git revision was retracted. This blocked every
go tooling invocation on the operator module (vet, build, test,
controller-gen). go.sum had also never been committed.

Bump to v0.0.0-20240228011516-70dd3763d340 (a real pseudo-version
contemporaneous with controller-runtime v0.19.0 / apimachinery
v0.31.0) and run `go mod tidy` to regenerate go.sum.

go mod tidy also bumps the language version directive to 1.23.0
(toolchain go1.24.6) — the older go 1.22 directive was inconsistent
with the toolchain image the Containerfile uses.

After this commit:
  go vet ./...       — clean
  go build ./...     — clean

Surfaced while preparing PR-49 (API & CRD foundations); separating
into its own commit so the build-hygiene fix is reviewable on its
own.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… updates

Adds a new validating + defaulting webhook for AgentCollective parallel
to the existing one for AgentCorpus. The webhook plugs the gap left by
loosening the AgentRole enum to a regex: schema-level validation now
accepts any DNS-label-style string, but the webhook rejects role names
not present in the operator's compiled-in catalogue.

Catalogue lookup uses internal/rolecatalogue.IsKnown(); rejection
messages include up to 3 closest matches by Levenshtein distance via
.Suggest(), so a typo like "research_plan" gets

  role "research_plan" is not in the operator's known-roles
  catalogue; did you mean "research_planner", "research_critic",
  "research_economist"?

instead of an opaque schema rejection.

Webhook also enforces:
  - roles in spec.scaling.roleScaling[*].role must be declared in
    spec.agents[*] (otherwise the override is dead config)
  - minReplicas <= maxReplicas
  - llm sub-struct present matches the chosen llm.backend

agentcorpus_webhook.go updates:
  - Default ManifestDelivery to "all" when unset (PR-50 reconciler
    relies on this).
  - Default each MCPServerSpec.Replicas to 1 and .Port to 8080.
  - Reject duplicate MCP server names — PR-51 reconciler derives
    Service names from MCPServerSpec.Name and collisions are fatal.

Verified via `go vet ./...` and `go build ./...` (both clean).

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 task 7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Output of `make manifests generate` after the API changes earlier in
this branch. Reviewable as a deterministic delta of the previous five
commits.

CRD changes:
  acc.redhat.io_agentcollectives.yaml
    - spec.agents[].role: closed enum dropped, replaced by
      pattern ^[a-z][a-z0-9_]{1,62}$ + length bounds (semantic
      check moves to the AgentCollective webhook).
    - spec.scaling.roleScaling[].role: same loosening.

  acc.redhat.io_agentcorpora.yaml
    - spec.mcpServers[]: new MCPServerSpec slice schema.
    - spec.manifestDelivery: new enum field {all|none}, default all.
    - status.mcpServerStatuses: new map keyed by MCP server name.
    - status.manifestDeliveryReady: new bool.

zz_generated.deepcopy.go regenerated with DeepCopy methods for the
two new types (MCPServerSpec, MCPServerStatus) and the slice/map
wiring in AgentCorpusSpec and AgentCorpusStatus.

config/webhook/manifests.yaml gains the AgentCollective mutating +
validating webhook entries (paths /mutate- and /validate-...
agentcollective).

Also adds operator/bin/ to .gitignore — the Makefile's controller-gen,
kustomize, and envtest targets land binaries there per developer.

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 task 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds operator/test/unit/role_catalogue_test.go covering the public API
of internal/rolecatalogue:

  IsKnown
    - legacy ACCv3 5-role set still admitted (regression guard)
    - D3 coding-split-skills personas + umbrella admitted
    - E4 autoresearcher personas admitted
    - empty / fnord / case-shifted / reserved (_base, TEMPLATE)
      reliably rejected

  All
    - sorted, deduped, fresh slice (mutation isolation contract)

  Suggest
    - returns the intended target for realistic typos:
      research_plan → research_planner
      research_economis → research_economist
      coding_agnt_architect → coding_agent_architect
      coding_agent_implmenter → coding_agent_implementer
      analyzt → analyst
      observator → observer
    - n cap honoured
    - n <= 0 returns nil (documented contract)
    - distance > 8 cutoff suppresses nonsense matches
      ("qqq..." input yields zero suggestions)

go test ./test/unit/... runs clean (existing tests unaffected).

Refs openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
PR-49 task 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All ten PR-49 tasks landed across commits 466273b, 9052391, d9c2784,
24e5414, d2043d0, the rolecatalogue commit, 1d61a17, and b325344.
Updates the lock board to status=ready-for-review and points readers
at the PR for review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@flg77
flg77 marked this pull request as ready for review May 9, 2026 06:02
@flg77
flg77 merged commit aa7a0bc into main May 9, 2026
@flg77
flg77 deleted the feat/op-pr49-api-foundations branch May 9, 2026 19:26
flg77 added a commit that referenced this pull request Jun 11, 2026
…49)

Net-new MLFlow layer complementary to the OTel trace export already
shipped (20260527-mlflow-otel-telemetry Phases 1-4): turns a
golden-prompt / eval / reasoning-bench execution into an MLFlow run so
"benchmark after a role or model change" lands in MLFlow's
experiment-comparison UI.

acc/backends/mlflow_runs.py (new, opt-in + lazy + best-effort):
  - enabled() — true only when ACC_MLFLOW_TRACKING_URI set AND mlflow
    importable; import acc.backends.mlflow_runs is always safe.
  - mlflow_run(...) ctxmanager — yields None (no-op) when disabled.
  - log_golden_results(results, *, run_meta) — one run per suite;
    params from run_meta (model/host/git_sha), metrics golden.count /
    passed / pass_rate + per-prompt pass.<name> + latency_ms.<name>.
    Reuses the exact GoldenResult shape — no new data model.
  - log_eval_outcome(...), log_reasoning_depth(...) for the evals +
    acc-bench consumers.
  Key/param sanitisation + length clipping for MLFlow limits; a
  tracking-server outage logs + returns False, never raises.

Wired beside acc/golden_prompts.py::persist_results (same run_meta keeps
the JSONL history + MLFlow in sync); best-effort try/except mirrors the
JSONL writer. Logs from the runner/CLI/scheduled side, never the agent
hot path.

pyproject.toml — mlflow-skinny added to the existing [mlflow] extra
(client-only; keeps the extra light).

tests/test_mlflow_runs.py — 10 tests: no-op/disabled paths,
recorded-call via an injected fake mlflow, tracking-outage resilience,
key sanitisation, persist_results integration (JSONL + run both fire).

Sweep: 3071 passed, 69 skipped (delta +10).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant