Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,9 @@ runs/
# land here when the operator/Makefile downloads them. Refetched per
# developer.
operator/bin/

# Operator manifest mirror: build-time copy of repo-root roles/, skills/,
# mcps/ trees so `//go:embed` in
# operator/internal/reconcilers/manifests/ can reach them. Repopulated
# by `make sync-manifests`.
operator/internal/reconcilers/manifests/data/
26 changes: 13 additions & 13 deletions openspec/changes/20260508-operator-feature-parity-d-e/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ Status mirror: `Status.MCPServerStatuses map[string]MCPServerStatus` with `Ready
func (r *AgentCorpusReconciler) buildSubReconcilers() []reconcilers.SubReconciler {
return []reconcilers.SubReconciler{
&reconcilers.PrerequisiteReconciler{...},
&manifests.ManifestDeliveryReconciler{...}, // NEW (PR-50) — slot 2
&manifests.ManifestDeliveryReconciler{...}, // NEW (PR-51) — slot 2
&reconcilers.UpgradeReconciler{...},
&infra.NATSReconciler{...},
&infra.RedisReconciler{...},
&infra.MilvusReconciler{},
&governance.OPABundleServerReconciler{...},
&governance.GatekeeperReconciler{...},
&bridge.KafkaBridgeReconciler{...},
&mcp.MCPServerReconciler{...}, // NEW (PR-51)
&mcp.MCPServerReconciler{...}, // NEW (PR-52)
&observability.OTelCollectorReconciler{...},
&observability.PrometheusRulesReconciler{...},
&collectiverec.CollectiveReconciler{...},
Expand Down Expand Up @@ -135,31 +135,31 @@ Modified:
- `operator/api/v1alpha1/agentcollective_types.go` (PR-49 — `}` fix + enum loosening)
- `operator/api/v1alpha1/agentcorpus_types.go` (PR-49 — MCP + delivery fields)
- `operator/api/v1alpha1/common_types.go` (PR-49 — enum loosening + role consts)
- `operator/internal/controller/agentcorpus_controller.go` (PR-50/51 — wire-in)
- `operator/internal/reconcilers/collective/agent_deployment.go` (PR-50 — volume + env)
- `operator/bundle/manifests/acc-operator.clusterserviceversion.yaml` (PR-52)
- `operator/config/samples/acc_tui_deployment.yaml` (PR-50 — TUI parity)
- `operator/internal/controller/agentcorpus_controller.go` (PR-51/52 — wire-in)
- `operator/internal/reconcilers/collective/agent_deployment.go` (PR-51 — volume + env)
- `operator/bundle/manifests/acc-operator.clusterserviceversion.yaml` (PR-53)
- `operator/config/samples/acc_tui_deployment.yaml` (PR-51 — TUI parity)

New:
- `operator/internal/rolecatalogue/catalogue.go` + `operator/hack/gen-catalogue.go` (PR-49)
- `operator/api/v1alpha1/agentcollective_webhook.go` (PR-49)
- `operator/internal/reconcilers/manifests/delivery.go` (PR-50)
- `operator/internal/reconcilers/mcp/server.go` (+ test) (PR-51)
- `operator/config/samples/acc_v1alpha1_agentcorpus_{autoresearcher,coding_split}.yaml` (PR-52)
- `operator/hack/test-kind.sh` (PR-52)
- `operator/internal/reconcilers/manifests/delivery.go` (PR-51)
- `operator/internal/reconcilers/mcp/server.go` (+ test) (PR-52)
- `operator/config/samples/acc_v1alpha1_agentcorpus_{autoresearcher,coding_split}.yaml` (PR-53)
- `operator/hack/test-kind.sh` (PR-53)

## Verification

- **PR-49**: `make generate manifests` is a no-op after the `}` fix; existing samples still
apply; new `operator/test/unit/role_catalogue_test.go` covers catalogue membership and
closest-match suggestions.
- **PR-50**: envtest creates the legacy `sol-corpus` sample, asserts `acc-roles` /
- **PR-51**: envtest creates the legacy `sol-corpus` sample, asserts `acc-roles` /
`acc-skills` / `acc-mcps` ConfigMaps exist with key counts equal to
`find roles -type f | wc -l` etc.; agent Deployment carries the three env vars + three
volumes; `manifest_delivery_test.go` exercises a small fixture FS.
- **PR-51**: envtest creates a corpus with `mcpServers: [{name: web-fetch, image: ...}]`,
- **PR-52**: envtest creates a corpus with `mcpServers: [{name: web-fetch, image: ...}]`,
asserts Deployment + Service `acc-mcp-web-fetch` exist; status populated.
- **PR-52**: end-to-end on kind via `hack/test-kind.sh`:
- **PR-53**: end-to-end on kind via `hack/test-kind.sh`:
```
kind create cluster --name acc-operator-test
make install deploy IMG=...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
| Change ID | 20260508-operator-feature-parity-d-e |
| Date | 2026-05-08 |
| Status | Draft |
| PR slots | this proposal lands as #48; implementation in #49 #52 |
| PR slots | this proposal lands as #48; implementation in #49, #51, #52, #53 (drift: an unrelated docs PR took #50) |
| Depends on | D1–D6 (coding-split-skills, PRs #34–#40), E1–E6 (autoresearcher, PRs #41–#46) |

---
Expand Down
96 changes: 57 additions & 39 deletions openspec/changes/20260508-operator-feature-parity-d-e/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,49 +74,67 @@

---

## PR-50 — Manifest delivery reconciler
## PR-51 — Manifest delivery reconciler

**Branch suggestion**: `feat/op-pr50-manifest-delivery`
**PR slot claimed by**: `-`
**Status**: `unstarted`
**Blocks on**: PR-49 merged (uses the new `ManifestDelivery` field)
**Branch**: `feat/op-pr50-manifest-delivery` (named for the originally-expected slot)
**PR**: https://github.com/flg77/acc/pull/51 (draft → ready 2026-05-09)
**PR slot claimed by**: `acc1` (host 10.199.12.91 — Claude instance)
**Status**: `ready-for-review` since 2026-05-09
**Blocks on**: PR-49 merged ✅ (uses the new `ManifestDelivery` field)

- [x] **Build-side prep** (out-of-plan but required): `make sync-manifests` target
mirrors repo-root `roles/`, `skills/`, `mcps/` into
`operator/internal/reconcilers/manifests/data/` so `//go:embed` can reach them.
Filters out `__pycache__`, `*.pyc`, `*.pyo`, `.DS_Store` to keep the round-trip
flat-key contract safe. Gitignored. Wired as a prereq for `generate`, `build`,
`run`, `test`, `test-unit`, `docker-build`. Commit `baa60f0`. *— claimed by: acc1*

- [x] **Implement `ManifestDeliveryReconciler`** at
`operator/internal/reconcilers/manifests/delivery.go`. `//go:embed all:data/{roles,
skills,mcps}`. Upsert three corpus-namespace ConfigMaps (`{corpus}-acc-roles`,
`{corpus}-acc-skills`, `{corpus}-acc-mcps`) via `util.Upsert`. Keys flatten `/` to
`__`. Public helpers (`FlattenPath`, `UnflattenKey`, `ConfigMapName`, `Suffixes`,
`RolesMountPath` / `SkillsMountPath` / `MCPsMountPath` consts) so
agent_deployment can reference the same naming and path rules. Commit `72a8650`.
*— claimed by: acc1*

- [ ] **Implement `ManifestDeliveryReconciler`** at
`operator/internal/reconcilers/manifests/delivery.go`. `embed.FS` over `roles/`,
`skills/`, `mcps/`. Upsert three corpus-namespace ConfigMaps (`acc-roles`,
`acc-skills`, `acc-mcps`) via `util.Upsert`. Keys flatten `/` to `__`; carry the
`items[]` projection list alongside so the volume mount re-projects to slash-paths.
*— claimed by: -*
- [x] **Wire reconciler into chain**: slot 2 in
`agentcorpus_controller.go::buildSubReconcilers()` (after `PrerequisiteReconciler`,
before `UpgradeReconciler`). Commit `93783ee`. *— claimed by: acc1*

- [ ] **Wire reconciler into chain**: `operator/internal/controller/agentcorpus_controller.go`
— slot the new reconciler #2 (after `PrerequisiteReconciler`, before
`UpgradeReconciler`). *— claimed by: -*

- [ ] **Inject volumes/env in agent pods**:
`operator/internal/reconcilers/collective/agent_deployment.go`. Append three
`VolumeMount`s (`/etc/acc/roles`, `/etc/acc/skills`, `/etc/acc/mcps`, all read-only),
three `Volume`s referencing the corpus-scoped CMs with `items[]` projection, three env
vars (`ACC_ROLES_ROOT`, `ACC_SKILLS_ROOT`, `ACC_MCPS_ROOT`). Gate on
`corpus.Spec.ManifestDelivery != "none"`. *— claimed by: -*

- [ ] **TUI parity**: edit `operator/config/samples/acc_tui_deployment.yaml` to add the
same three env vars and `acc-roles` / `acc-skills` volume mounts. *— claimed by: -*

- [ ] **Unit + envtest coverage**:
- `manifest_delivery_test.go` with a 3-role embed.FS fixture.
- Extend `agentcorpus_controller_test.go` to assert the legacy `sol-corpus` sample
produces all three ConfigMaps with key counts equal to `find roles -type f | wc -l`,
`find skills -type f | wc -l`, `find mcps -type f | wc -l`.
*— claimed by: -*
- [x] **Inject volumes/env in agent pods**: new `buildManifestDelivery` helper in
`agent_deployment.go` reads each ConfigMap from the API client and projects keys
into `items[]: [{key, path: UnflattenKey(key)}]`. Appends three `VolumeMount`s,
three `Volume`s, three env vars (`ACC_ROLES_ROOT`, `ACC_SKILLS_ROOT`,
`ACC_MCPS_ROOT`). Returns empty slices when `spec.manifestDelivery == "none"` or
when a CM is not yet present (graceful first-cycle handling). Commit `a6ac7c1`.
*— claimed by: acc1*

- [ ] **Manual contract check**: `kubectl get cm acc-roles -o jsonpath='{.data}' | jq
'keys|length'` equals `find roles -type f | wc -l`. *— claimed by: -*
- [x] **TUI parity**: `acc_tui_deployment.yaml` gains `ACC_ROLES_ROOT` /
`ACC_SKILLS_ROOT` env, `acc-roles` / `acc-skills` ConfigMap mounts at
`/etc/acc/{roles,skills}` (read-only, `optional: true` to tolerate first-apply
race). Commit `adf1f90`. *— claimed by: acc1*

- [x] **Unit tests** (8 total across two packages, all PASS):
- `internal/reconcilers/manifests/delivery_test.go` — walkTree spot-checks
legacy + D3 + E4 personas; round-trip safety across all three trees;
FlattenPath/UnflattenKey direct cases; suffix contract.
- `test/unit/manifest_delivery_test.go` — black-box Reconcile against
controller-runtime fake client. Asserts three CMs created with non-empty
Data + operator-managed labels; keys never contain "/"; opt-out short-
circuits (no CMs); two passes are idempotent.
Commit `6ba5b0f`. *— claimed by: acc1*

- [x] **Manual contract check**: deferred — the manifest reconciler test
asserts the same property (CM `Data` size matches the embedded tree) inside
the test harness, removing the need for a live `kubectl get cm` step.
Documented for completeness in PR description. *— claimed by: acc1*

---

## PR-51 — MCP server reconciler
## PR-52 — MCP server reconciler

**Branch suggestion**: `feat/op-pr51-mcp-reconciler`
**Branch suggestion**: `feat/op-pr52-mcp-reconciler`
**PR slot claimed by**: `-`
**Status**: `unstarted`
**Blocks on**: PR-49 merged (uses `MCPServerSpec`)
Expand Down Expand Up @@ -144,12 +162,12 @@

---

## PR-52 — Demo samples + CSV update
## PR-53 — Demo samples + CSV update

**Branch suggestion**: `feat/op-pr52-demo-samples`
**Branch suggestion**: `feat/op-pr53-demo-samples`
**PR slot claimed by**: `-`
**Status**: `unstarted`
**Blocks on**: PR-49, PR-50, PR-51 merged
**Blocks on**: PR-49, PR-51, PR-52 merged

- [ ] **Autoresearcher sample**:
`operator/config/samples/acc_v1alpha1_agentcorpus_autoresearcher.yaml`. `AgentCorpus` +
Expand Down Expand Up @@ -182,7 +200,7 @@

## Optional — PR-A (parallel hardening)

Not blocking the main sequence; can run in parallel with PR-50 or PR-51.
Not blocking the main sequence; can run in parallel with PR-51 or PR-52.

- [ ] Add `jsonschema` to `Containerfile.agent-core` (`microdnf install`) so role/skill
schema validation is strict in cluster — `acc/skills/registry.py:52-64` falls back
Expand Down
36 changes: 30 additions & 6 deletions operator/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,36 @@ help: ## Display this help.

##@ Development

# MANIFEST_DATA_DIR holds copies of the repo-root roles/, skills/, mcps/
# trees inside the operator module so `//go:embed` can reach them. The
# directory is gitignored — it's a build-time mirror, not source.
MANIFEST_DATA_DIR := internal/reconcilers/manifests/data

.PHONY: sync-manifests
sync-manifests: ## Mirror repo-root roles/, skills/, mcps/ into the operator module for go:embed.
@mkdir -p $(MANIFEST_DATA_DIR)
@for tree in roles skills mcps; do \
if [ -d ../$$tree ]; then \
rm -rf $(MANIFEST_DATA_DIR)/$$tree && \
cp -R ../$$tree $(MANIFEST_DATA_DIR)/$$tree && \
find $(MANIFEST_DATA_DIR)/$$tree \( \
-type d -name __pycache__ \
-o -name '*.pyc' \
-o -name '*.pyo' \
-o -name '.DS_Store' \
\) -exec rm -rf {} +; \
else \
echo "warning: ../$$tree does not exist; skipping"; \
fi; \
done
@find $(MANIFEST_DATA_DIR) -type f | wc -l | xargs printf "sync-manifests: %s files mirrored into $(MANIFEST_DATA_DIR)\n"

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CRD objects.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases output:rbac:artifacts:config=config/rbac

.PHONY: generate
generate: controller-gen ## Generate code (DeepCopy, etc.)
generate: controller-gen sync-manifests ## Generate code (DeepCopy, etc.) and refresh embedded manifest mirror.
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
Expand All @@ -56,25 +80,25 @@ vet: ## Run go vet.
go vet ./...

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
test: manifests generate fmt vet envtest sync-manifests ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-path $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out

.PHONY: test-unit
test-unit: ## Run unit tests only (no envtest).
test-unit: sync-manifests ## Run unit tests only (no envtest).
go test ./test/unit/... -v

##@ Build

.PHONY: build
build: manifests generate fmt vet ## Build operator binary.
build: manifests generate fmt vet sync-manifests ## Build operator binary.
go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run the operator locally against the configured Kubernetes cluster.
run: manifests generate fmt vet sync-manifests ## Run the operator locally against the configured Kubernetes cluster.
go run ./cmd/main.go

.PHONY: docker-build
docker-build: ## Build container image.
docker-build: sync-manifests ## Build container image (data/ tree must be in place before COPY).
docker build -t ${IMG} .

.PHONY: docker-push
Expand Down
42 changes: 40 additions & 2 deletions operator/config/samples/acc_tui_deployment.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
# ACC TUI — optional Kubernetes Deployment
# Deploys the acc-tui terminal UI as a pod alongside the collective.
# The TUI connects to NATS by internal DNS and requires no storage volumes.
# The TUI connects to NATS by internal DNS for live data and reads
# the operator-managed acc-roles / acc-skills ConfigMaps for the
# Ecosystem screen (role-md tooling, skill manifest browsing).
#
# Usage:
# kubectl apply -f operator/config/samples/acc_tui_deployment.yaml
#
# Prerequisites:
# - acc-config ConfigMap created by the operator for the target collective
# - acc-roles / acc-skills ConfigMaps emitted by the operator's
# ManifestDeliveryReconciler (these land automatically when an
# AgentCorpus with spec.manifestDelivery=all is reconciled)
# - NATS service reachable at nats:4222 within the namespace
#
# NOTE on ConfigMap key naming: the operator flattens "/" in source paths
# to "__" because Kubernetes rejects "/" in ConfigMap keys. Without an
# items[] projection (which would have to enumerate every key), this
# sample mounts the CMs flat — a TUI screen that needs the original
# nested-directory shape should call the role_loader's "operator-mode"
# helper, which knows to split "__" back to "/".

apiVersion: apps/v1
kind: Deployment
Expand Down Expand Up @@ -57,6 +69,15 @@ spec:
name: sol-01-acc-config
key: collective_id
optional: true
# Operator-mode manifest roots: parity with
# container/production/podman-compose.yml (TUI service,
# ACC_ROLES_ROOT=/app/roles). The operator emits the CMs
# under {corpus}-acc-{roles,skills} so the Ecosystem screen
# has data to render.
- name: ACC_ROLES_ROOT
value: /etc/acc/roles
- name: ACC_SKILLS_ROOT
value: /etc/acc/skills
resources:
requests:
cpu: "50m"
Expand All @@ -70,8 +91,25 @@ spec:
drop:
- ALL
readOnlyRootFilesystem: true
# No storage volumes required (TUI is stateless — all data from NATS)
volumeMounts:
- name: acc-roles
mountPath: /etc/acc/roles
readOnly: true
- name: acc-skills
mountPath: /etc/acc/skills
readOnly: true
stdin: true
tty: true # required for Textual terminal rendering
volumes:
# Adjust the ConfigMap names to match your AgentCorpus.metadata.name —
# the operator emits {corpus}-acc-roles and {corpus}-acc-skills.
- name: acc-roles
configMap:
name: sol-corpus-acc-roles
optional: true # tolerate first-apply race
- name: acc-skills
configMap:
name: sol-corpus-acc-skills
optional: true
restartPolicy: Always
terminationGracePeriodSeconds: 10
1 change: 1 addition & 0 deletions operator/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ require (
golang.org/x/time v0.6.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apiextensions-apiserver v0.31.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions operator/internal/controller/agentcorpus_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
collectiverec "github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/reconcilers/collective"
"github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/reconcilers/governance"
"github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/reconcilers/infra"
"github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/reconcilers/manifests"
"github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/reconcilers/observability"
statuspkg "github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/status"
)
Expand Down Expand Up @@ -186,6 +187,11 @@ func (r *AgentCorpusReconciler) Reconcile(ctx context.Context, req ctrl.Request)
func (r *AgentCorpusReconciler) buildSubReconcilers() []reconcilers.SubReconciler {
return []reconcilers.SubReconciler{
&reconcilers.PrerequisiteReconciler{Client: r.Client, Discovery: r.Discovery},
// ManifestDelivery slot 2: emits the corpus-scoped acc-roles /
// acc-skills / acc-mcps ConfigMaps that every collective's agent
// Deployment mounts. Must run before UpgradeReconciler so the CMs
// exist before upgrade pods reference them.
&manifests.ManifestDeliveryReconciler{Client: r.Client, Scheme: r.Scheme},
&reconcilers.UpgradeReconciler{Client: r.Client},
&infra.NATSReconciler{Client: r.Client, Scheme: r.Scheme},
&infra.RedisReconciler{Client: r.Client, Scheme: r.Scheme},
Expand Down
Loading