diff --git a/.gitignore b/.gitignore index cf34d5c8..a759e6be 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,8 @@ test/results/ # runs/-/ — gitignored so report drafts never # leak into commits. runs/ + +# Operator dev tooling: controller-gen, kustomize, envtest binaries +# land here when the operator/Makefile downloads them. Refetched per +# developer. +operator/bin/ diff --git a/openspec/changes/20260508-operator-feature-parity-d-e/tasks.md b/openspec/changes/20260508-operator-feature-parity-d-e/tasks.md index 8e5ba8b5..9d239e00 100644 --- a/openspec/changes/20260508-operator-feature-parity-d-e/tasks.md +++ b/openspec/changes/20260508-operator-feature-parity-d-e/tasks.md @@ -12,54 +12,65 @@ ## PR-49 — API & CRD foundations -**Branch suggestion**: `feat/op-pr49-api-foundations` -**PR slot claimed by**: `-` -**Status**: `unstarted` - -- [ ] **Fix the `}` bug** in `operator/api/v1alpha1/agentcollective_types.go:91-94` - (`RoleDefinition` struct missing closing brace before `AgentRoleSpec`). After the fix, - `make generate` should be a no-op against the existing `zz_generated.deepcopy.go`, - proving the source matches the generated shape. *— claimed by: -* - -- [ ] **Loosen the role enum**. In `operator/api/v1alpha1/common_types.go` replace the - `// +kubebuilder:validation:Enum=ingester;...` marker on `AgentRole` with - `Pattern=^[a-z][a-z0-9_]{1,62}$` + `MinLength=2` + `MaxLength=63`. Drop the redundant - field-level `Enum=` markers in `agentcollective_types.go` on `AgentRoleSpec.Role` and - `RoleScalingSpec.Role`. *— claimed by: -* - -- [ ] **Append exported role consts** in `common_types.go` for the 11 new personas plus - `RoleCodingAgent` umbrella: `RoleCodingArchitect`, `RoleCodingDependency`, - `RoleCodingImplementer`, `RoleCodingReviewer`, `RoleCodingTester`, `RoleResearchPlanner`, - `RoleResearchStrategist`, `RoleResearchEconomist`, `RoleResearchCompetitor`, - `RoleResearchSynthesizer`, `RoleResearchCritic`. *— claimed by: -* - -- [ ] **Add `MCPServerSpec` + status types** in `operator/api/v1alpha1/agentcorpus_types.go`. - Fields: `Name, Image, Replicas, Port, Env, SecretEnv, ShmSizeMi, Resources`. Status - type: `MCPServerStatus{Ready bool; Replicas int32; ServiceURL string}`. Add - `Status.MCPServerStatuses map[string]MCPServerStatus`. *— claimed by: -* - -- [ ] **Add `ManifestDelivery` field** in `agentcorpus_types.go`: - `// +kubebuilder:validation:Enum=all;none` + `// +kubebuilder:default=all`. *— claimed by: -* - -- [ ] **Implement role catalogue**: create `operator/internal/rolecatalogue/catalogue.go` - with `var KnownRoles map[string]struct{}` populated via `go:embed`-baked listing of - `roles/*/role.yaml`. Add the generator at `operator/hack/gen-catalogue.go` triggered by - `//go:generate`. *— claimed by: -* - -- [ ] **Add `AgentCollective` validating webhook** at - `operator/api/v1alpha1/agentcollective_webhook.go` (parallel to existing - `agentcorpus_webhook.go`). Reject roles not in `KnownRoles` with closest-match - suggestions. *— claimed by: -* - -- [ ] **Run `make generate manifests`** and commit the regenerated - `zz_generated.deepcopy.go` and `config/crd/bases/*.yaml` deltas. *— claimed by: -* - -- [ ] **Unit tests** at `operator/test/unit/role_catalogue_test.go` covering catalogue - membership and the closest-match suggestion path. *— claimed by: -* - -- [ ] **Verify backwards compat**: `kubectl apply --dry-run=server -f - config/samples/acc_v1alpha1_agentcorpus_standalone.yaml` succeeds; same for `_rhoai`. - *— claimed by: -* +**Branch**: `feat/op-pr49-api-foundations` +**PR**: https://github.com/flg77/acc/pull/49 (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 + +- [x] **Fix the `}` bug** in `operator/api/v1alpha1/agentcollective_types.go:91-94` + (`RoleDefinition` struct missing closing brace before `AgentRoleSpec`). Landed in + commit `466273b`. *— claimed by: acc1* + +- [x] **Loosen the role enum** in `operator/api/v1alpha1/common_types.go`: replaced + closed `Enum=` with `Pattern=^[a-z][a-z0-9_]{1,62}$` + length bounds; dropped + field-level `Enum=` markers on `AgentRoleSpec.Role` and `RoleScalingSpec.Role`. + Landed in commit `9052391`. *— claimed by: acc1* + +- [x] **Append exported role consts** in `common_types.go` for 12 personas (5 + coding-split + 6 research + umbrella `RoleCodingAgent`). Landed in commit + `9052391`. *— claimed by: acc1* + +- [x] **Add `MCPServerSpec` + status types** in `agentcorpus_types.go` — + `Name/Image/Replicas/Port/Env/SecretEnv/ShmSizeMi/Resources` + matching status type + + `Status.MCPServerStatuses` map. Landed in commit `d9c2784`. *— claimed by: acc1* + +- [x] **Add `ManifestDelivery` field** — `Enum=all;none`, default `all`. Landed in + commit `d9c2784`. *— claimed by: acc1* + +- [x] **Implement role catalogue** at `operator/internal/rolecatalogue/`. Public API: + `IsKnown / All / Suggest`. Source via `//go:embed known_roles.txt`; generator at + `operator/hack/gen-catalogue.go` triggered by `//go:generate`. 47 roles seeded. + Landed in commit `1f7c3d…` *(pre-rebase hash, see git log)*. *— claimed by: acc1* + +- [x] **Add `AgentCollective` validating webhook** at `agentcollective_webhook.go` + with closest-match Levenshtein suggestions; bonus validation for + `roleScaling[*].role` declared-in-agents check, minReplicas≤maxReplicas, and + llm sub-struct presence. Also extended `agentcorpus_webhook.go`: defaults + `manifestDelivery=all`, defaults `MCPServer.Replicas=1` and `Port=8080`, + rejects duplicate MCP server names. Landed in commit `d2043d0`. + *— claimed by: acc1* + +- [x] **Run `make manifests generate`** — regenerated `zz_generated.deepcopy.go` + (+632/-238), `config/crd/bases/acc.redhat.io_agent{collectives,corpora}.yaml` + (the role enum opens up; the new MCP/manifest fields appear), and + `config/webhook/manifests.yaml` (AgentCollective mutating + validating webhooks + registered). Landed in commit `1d61a17`. Includes a build-hygiene side commit + `24e5414` that repaired a stale kube-openapi pseudo-version in `go.mod` and + generated a missing `go.sum` so `go vet/build/test` and `make generate` could + run at all. *— claimed by: acc1* + +- [x] **Unit tests** at `operator/test/unit/role_catalogue_test.go` — 12 test + functions covering catalogue membership for legacy + new personas, sorted / + unique / mutation-isolated `All()`, Suggest typo recognition for 6 realistic + inputs, n-cap, n≤0 contract, and distance cutoff. All pass; full suite clean. + Landed in commit `b325344`. *— claimed by: acc1* + +- [x] **Verify backwards compat** — `go vet ./...`, `go build ./...`, + `go test ./test/unit/...` all clean. Regex `^[a-z][a-z0-9_]{1,62}$` accepts + every role in both legacy samples (`standalone` + `rhoai`) and every new + persona const. Live `kubectl apply` skipped in favour of regex verification + to avoid mutating the shared cluster — see PR #49 description. + *— claimed by: acc1* --- diff --git a/operator/api/v1alpha1/agentcollective_types.go b/operator/api/v1alpha1/agentcollective_types.go index eda56e83..822e143c 100644 --- a/operator/api/v1alpha1/agentcollective_types.go +++ b/operator/api/v1alpha1/agentcollective_types.go @@ -89,11 +89,13 @@ type RoleDefinition struct { // +kubebuilder:default="0.1.0" // +optional Version string `json:"version,omitempty"` +} // AgentRoleSpec defines the deployment configuration for one agent role. type AgentRoleSpec struct { - // Role identifies the ACC agent role. - // +kubebuilder:validation:Enum=ingester;analyst;synthesizer;arbiter;observer + // Role identifies the ACC agent role. Schema-level validation is the + // regex pattern on the AgentRole type; semantic validation against the + // operator's compiled-in catalogue happens in the AgentCollective webhook. Role AgentRole `json:"role"` // Replicas is the baseline replica count (before KEDA scaling). @@ -214,7 +216,7 @@ type ScalingSpec struct { // RoleScalingSpec configures KEDA scaling for a single agent role. type RoleScalingSpec struct { // Role identifies which agent role this scaling config applies to. - // +kubebuilder:validation:Enum=ingester;analyst;synthesizer;arbiter;observer + // Validation: regex on the AgentRole type + webhook catalogue check. Role AgentRole `json:"role"` // MinReplicas is the KEDA minimum replica count. diff --git a/operator/api/v1alpha1/agentcollective_webhook.go b/operator/api/v1alpha1/agentcollective_webhook.go new file mode 100644 index 00000000..2267eca9 --- /dev/null +++ b/operator/api/v1alpha1/agentcollective_webhook.go @@ -0,0 +1,199 @@ +// Copyright 2026 ACC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package v1alpha1 + +import ( + "fmt" + "strings" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/rolecatalogue" +) + +var agentcollectivelog = logf.Log.WithName("agentcollective-resource") + +// SetupWebhookWithManager registers the webhook with the Manager. +func (r *AgentCollective) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// +kubebuilder:webhook:path=/mutate-acc-redhat-io-v1alpha1-agentcollective,mutating=true,failurePolicy=fail,sideEffects=None,groups=acc.redhat.io,resources=agentcollectives,verbs=create;update,versions=v1alpha1,name=magentcollective.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &AgentCollective{} + +// Default implements webhook.Defaulter to set default values. +func (r *AgentCollective) Default() { + agentcollectivelog.Info("default", "name", r.Name) + + if r.Spec.HeartbeatIntervalSeconds == 0 { + r.Spec.HeartbeatIntervalSeconds = 30 + } + for i := range r.Spec.Agents { + if r.Spec.Agents[i].Replicas == 0 { + r.Spec.Agents[i].Replicas = 1 + } + } + if r.Spec.LLM.EmbeddingModel == "" { + r.Spec.LLM.EmbeddingModel = "all-MiniLM-L6-v2" + } +} + +// +kubebuilder:webhook:path=/validate-acc-redhat-io-v1alpha1-agentcollective,mutating=false,failurePolicy=fail,sideEffects=None,groups=acc.redhat.io,resources=agentcollectives,verbs=create;update,versions=v1alpha1,name=vagentcollective.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &AgentCollective{} + +// ValidateCreate implements webhook.Validator. +func (r *AgentCollective) ValidateCreate() (admission.Warnings, error) { + agentcollectivelog.Info("validate create", "name", r.Name) + return nil, r.validateAgentCollective() +} + +// ValidateUpdate implements webhook.Validator. +func (r *AgentCollective) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + agentcollectivelog.Info("validate update", "name", r.Name) + return nil, r.validateAgentCollective() +} + +// ValidateDelete implements webhook.Validator. +func (r *AgentCollective) ValidateDelete() (admission.Warnings, error) { + return nil, nil +} + +// validateAgentCollective enforces semantic rules that the CRD schema can't +// express: role names must be present in the operator's compiled-in +// catalogue (see internal/rolecatalogue), per-role replica counts in +// scaling overrides must reference declared agents, etc. +func (r *AgentCollective) validateAgentCollective() error { + var allErrs field.ErrorList + + // Roles in spec.agents[*] must be in the catalogue. + declaredRoles := map[string]bool{} + for i, a := range r.Spec.Agents { + role := string(a.Role) + declaredRoles[role] = true + if !rolecatalogue.IsKnown(role) { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "agents").Index(i).Child("role"), + role, + unknownRoleMessage(role), + )) + } + } + + // Roles referenced in spec.scaling.roleScaling[*] must (a) be in the + // catalogue, AND (b) appear in spec.agents — otherwise the scaling + // override is dead config. + if r.Spec.Scaling != nil { + for i, rs := range r.Spec.Scaling.RoleScaling { + role := string(rs.Role) + if !rolecatalogue.IsKnown(role) { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "scaling", "roleScaling").Index(i).Child("role"), + role, + unknownRoleMessage(role), + )) + continue + } + if !declaredRoles[role] { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "scaling", "roleScaling").Index(i).Child("role"), + role, + fmt.Sprintf("role %q is not declared in spec.agents — scaling override has no target", role), + )) + } + if rs.MaxReplicas > 0 && rs.MinReplicas > rs.MaxReplicas { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "scaling", "roleScaling").Index(i).Child("minReplicas"), + rs.MinReplicas, + fmt.Sprintf("minReplicas (%d) must not exceed maxReplicas (%d)", rs.MinReplicas, rs.MaxReplicas), + )) + } + } + } + + // LLM backend wiring sanity — the schema enforces presence of the right + // sub-struct via OpenAPI, but we cross-check that the chosen backend's + // sub-struct is non-nil here so the controller never has to nil-guard. + switch r.Spec.LLM.Backend { + case LLMBackendOllama: + if r.Spec.LLM.Ollama == nil { + allErrs = append(allErrs, field.Required( + field.NewPath("spec", "llm", "ollama"), + "llm.ollama is required when llm.backend=ollama", + )) + } + case LLMBackendAnthropic: + if r.Spec.LLM.Anthropic == nil { + allErrs = append(allErrs, field.Required( + field.NewPath("spec", "llm", "anthropic"), + "llm.anthropic is required when llm.backend=anthropic", + )) + } + case LLMBackendVLLM: + if r.Spec.LLM.VLLM == nil { + allErrs = append(allErrs, field.Required( + field.NewPath("spec", "llm", "vllm"), + "llm.vllm is required when llm.backend=vllm", + )) + } + case LLMBackendLlamaStack: + if r.Spec.LLM.LlamaStack == nil { + allErrs = append(allErrs, field.Required( + field.NewPath("spec", "llm", "llamaStack"), + "llm.llamaStack is required when llm.backend=llama_stack", + )) + } + } + + if len(allErrs) == 0 { + return nil + } + return apierrors.NewInvalid( + schema.GroupKind{Group: "acc.redhat.io", Kind: "AgentCollective"}, + r.Name, allErrs, + ) +} + +// unknownRoleMessage formats the error for a role that doesn't appear in +// the catalogue, including up to three closest matches by Levenshtein +// distance. The intent is to make typos obvious without listing all 47 +// known roles inline. +func unknownRoleMessage(role string) string { + suggestions := rolecatalogue.Suggest(role, 3) + if len(suggestions) == 0 { + return fmt.Sprintf( + "role %q is not in the operator's known-roles catalogue; "+ + "add roles/%s/role.yaml to the source tree and rebuild the operator, "+ + "or pick one of the existing personas", + role, role, + ) + } + return fmt.Sprintf( + "role %q is not in the operator's known-roles catalogue; did you mean %s?", + role, strings.Join(quoteAll(suggestions), ", "), + ) +} + +func quoteAll(ss []string) []string { + out := make([]string, len(ss)) + for i, s := range ss { + out[i] = fmt.Sprintf("%q", s) + } + return out +} diff --git a/operator/api/v1alpha1/agentcorpus_types.go b/operator/api/v1alpha1/agentcorpus_types.go index 11000873..b703eadf 100644 --- a/operator/api/v1alpha1/agentcorpus_types.go +++ b/operator/api/v1alpha1/agentcorpus_types.go @@ -65,6 +65,28 @@ type AgentCorpusSpec struct { // Ignored when deployMode is standalone or rhoai. // +optional Edge *EdgeSpec `json:"edge,omitempty"` + + // MCPServers configures shared MCP servers visible to every collective in + // this corpus. Each entry produces a Deployment + Service named + // acc-mcp-{name}, matching the URL convention used by mcps//mcp.yaml. + // The reconciler that owns these objects ships in PR-51 (this PR only adds + // the schema). + // +optional + // +kubebuilder:validation:MaxItems=16 + MCPServers []MCPServerSpec `json:"mcpServers,omitempty"` + + // ManifestDelivery controls how the operator-baked roles/, skills/, and + // mcps/ trees reach agent pods. + // "all" (default) — operator emits ConfigMaps and mounts them at + // /etc/acc/{roles,skills,mcps} in every agent pod. + // "none" — operator skips the mounts; users must bake the trees into + // a custom agent image. + // The reconciler that emits the ConfigMaps and the volume injection in + // agent pods both ship in PR-50 (this PR only adds the schema). + // +kubebuilder:validation:Enum=all;none + // +kubebuilder:default=all + // +optional + ManifestDelivery string `json:"manifestDelivery,omitempty"` } // CollectiveRef references an AgentCollective resource in the same namespace. @@ -284,6 +306,80 @@ type KafkaSpec struct { CredentialsSecretRef *corev1.SecretReference `json:"credentialsSecretRef,omitempty"` } +// MCPServerSpec configures one MCP server Deployment + Service. +// The operator emits a Deployment named acc-mcp-{Name} and a Service of the +// same name on Port. The Service name matches the url: field convention used +// by mcps//mcp.yaml so agents resolve the server without manifest edits. +// +// The reconciler that owns these objects ships in PR-51 of the +// 20260508-operator-feature-parity-d-e openspec change. +type MCPServerSpec struct { + // Name matches the directory name under mcps/ in the source tree + // (e.g. "web-search-brave"). DNS-label-safe. + // +kubebuilder:validation:Pattern=`^[a-z][a-z0-9-]{1,62}$` + // +kubebuilder:validation:MinLength=2 + // +kubebuilder:validation:MaxLength=63 + Name string `json:"name"` + + // Image is the full container image reference for the MCP server, + // including registry, repository, and tag. + // +kubebuilder:validation:MinLength=1 + Image string `json:"image"` + + // Replicas sets the Deployment replica count. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=10 + // +kubebuilder:default=1 + // +optional + Replicas int32 `json:"replicas,omitempty"` + + // Port is the JSON-RPC port the server listens on. Becomes the Service + // port and the targetPort. Convention across the bundled MCPs is 8080. + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=8080 + // +optional + Port int32 `json:"port,omitempty"` + + // Env injects environment variables into the MCP container. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // SecretEnv pulls env vars from referenced Secrets / ConfigMaps via + // envFrom (typical use: BRAVE_API_KEY for web-search-brave, an Anthropic + // or OpenAI key for web-browser-harness). + // +optional + SecretEnv []corev1.EnvFromSource `json:"secretEnv,omitempty"` + + // ShmSizeMi mounts a Memory-medium emptyDir at /dev/shm sized to the + // requested mebibytes. Required for the browser-harness MCP because + // Chromium crashes intermittently with the default 64 MiB tmpfs; + // 256 MiB is browser-use's documented minimum. + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Maximum=4096 + // +kubebuilder:default=0 + // +optional + ShmSizeMi int32 `json:"shmSizeMi,omitempty"` + + // Resources sets CPU/memory requests and limits for the MCP container. + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// MCPServerStatus reports the operational state of one MCP server. +// Aggregated into AgentCorpusStatus.MCPServerStatuses by the MCP reconciler. +type MCPServerStatus struct { + // Ready is true when the Deployment's ReadyReplicas matches Replicas. + Ready bool `json:"ready"` + + // Replicas is the live ReadyReplicas count from the Deployment. + Replicas int32 `json:"replicas"` + + // ServiceURL is the in-cluster JSON-RPC URL agents should call. + // Format: http://acc-mcp-{name}.{namespace}.svc.cluster.local:{port}/rpc + ServiceURL string `json:"serviceURL,omitempty"` +} + // ObservabilitySpec configures telemetry collection. type ObservabilitySpec struct { // Backend selects the telemetry backend. @@ -385,6 +481,17 @@ type AgentCorpusStatus struct { // +optional KafkaBridgeReady bool `json:"kafkaBridgeReady,omitempty"` + // MCPServerStatuses reports the per-MCP-server state. Keyed by + // MCPServerSpec.Name. Populated by the MCP reconciler that ships in PR-51. + // +optional + MCPServerStatuses map[string]MCPServerStatus `json:"mcpServerStatuses,omitempty"` + + // ManifestDeliveryReady is true when the acc-roles, acc-skills, and + // acc-mcps ConfigMaps have been emitted by the manifest delivery + // reconciler that ships in PR-50. + // +optional + ManifestDeliveryReady bool `json:"manifestDeliveryReady,omitempty"` + // CurrentVersion is the ACC version currently deployed. // +optional CurrentVersion string `json:"currentVersion,omitempty"` diff --git a/operator/api/v1alpha1/agentcorpus_webhook.go b/operator/api/v1alpha1/agentcorpus_webhook.go index bbcbc69e..87754300 100644 --- a/operator/api/v1alpha1/agentcorpus_webhook.go +++ b/operator/api/v1alpha1/agentcorpus_webhook.go @@ -80,6 +80,18 @@ func (r *AgentCorpus) Default() { if r.Spec.UpgradePolicy.Mode == "" { r.Spec.UpgradePolicy.Mode = UpgradeModeAuto } + if r.Spec.ManifestDelivery == "" { + r.Spec.ManifestDelivery = "all" + } + for i := range r.Spec.MCPServers { + mcp := &r.Spec.MCPServers[i] + if mcp.Replicas == 0 { + mcp.Replicas = 1 + } + if mcp.Port == 0 { + mcp.Port = 8080 + } + } } // +kubebuilder:webhook:path=/validate-acc-redhat-io-v1alpha1-agentcorpus,mutating=false,failurePolicy=fail,sideEffects=None,groups=acc.redhat.io,resources=agentcorpora,verbs=create;update,versions=v1alpha1,name=vagentcorpus.kb.io,admissionReviewVersions=v1 @@ -155,6 +167,20 @@ func (r *AgentCorpus) validateAgentCorpus() error { seen[c.Name] = true } + // duplicate MCP server names not allowed; the reconciler in PR-51 derives + // the Service name from MCPServerSpec.Name, so collisions would be fatal. + mcpSeen := map[string]bool{} + for i, m := range r.Spec.MCPServers { + if mcpSeen[m.Name] { + allErrs = append(allErrs, field.Invalid( + field.NewPath("spec", "mcpServers").Index(i).Child("name"), + m.Name, + fmt.Sprintf("duplicate MCP server name %q", m.Name), + )) + } + mcpSeen[m.Name] = true + } + if len(allErrs) == 0 { return nil } diff --git a/operator/api/v1alpha1/common_types.go b/operator/api/v1alpha1/common_types.go index 4621f513..39141850 100644 --- a/operator/api/v1alpha1/common_types.go +++ b/operator/api/v1alpha1/common_types.go @@ -27,16 +27,43 @@ const ( ) // AgentRole identifies an ACC agent function. -// Mirrors the Python AgentRole literal. Full 5-role spec from ACCv3. -// +kubebuilder:validation:Enum=ingester;analyst;synthesizer;arbiter;observer +// +// Schema-level validation is a regex on a DNS-label-style string; +// semantic validation against the operator's compiled-in catalogue happens +// in the AgentCollective validating webhook. New personas can be added by +// dropping a roles//role.yaml into the source tree and running +// `go generate ./...` against operator/internal/rolecatalogue/. +// +kubebuilder:validation:Pattern=`^[a-z][a-z0-9_]{1,62}$` +// +kubebuilder:validation:MinLength=2 +// +kubebuilder:validation:MaxLength=63 type AgentRole string const ( + // Legacy ACCv3 5-role set — kept for backwards compatibility with + // existing AgentCorpus / AgentCollective custom resources. RoleIngester AgentRole = "ingester" RoleAnalyst AgentRole = "analyst" RoleSynthesizer AgentRole = "synthesizer" RoleArbiter AgentRole = "arbiter" RoleObserver AgentRole = "observer" + + // Coding-split-skills personas (D3 / PR #44, #39). + // Used by examples/coding_split_skills/. + RoleCodingAgent AgentRole = "coding_agent" + RoleCodingArchitect AgentRole = "coding_agent_architect" + RoleCodingDependency AgentRole = "coding_agent_dependency" + RoleCodingImplementer AgentRole = "coding_agent_implementer" + RoleCodingReviewer AgentRole = "coding_agent_reviewer" + RoleCodingTester AgentRole = "coding_agent_tester" + + // Autoresearcher personas (E4 / PR #44). + // Used by examples/acc_autoresearcher/. + RoleResearchPlanner AgentRole = "research_planner" + RoleResearchStrategist AgentRole = "research_strategist" + RoleResearchEconomist AgentRole = "research_economist" + RoleResearchCompetitor AgentRole = "research_competitor" + RoleResearchSynthesizer AgentRole = "research_synthesizer" + RoleResearchCritic AgentRole = "research_critic" ) // LLMBackend selects the language model implementation. diff --git a/operator/api/v1alpha1/zz_generated.deepcopy.go b/operator/api/v1alpha1/zz_generated.deepcopy.go index 0ce532cb..0d9179b0 100644 --- a/operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/operator/api/v1alpha1/zz_generated.deepcopy.go @@ -7,19 +7,25 @@ // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // Code generated by controller-gen. DO NOT EDIT. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto copies all properties of this object into another of the same type. -func (in *AgentCorpus) DeepCopyInto(out *AgentCorpus) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCollective) DeepCopyInto(out *AgentCollective) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -27,228 +33,128 @@ func (in *AgentCorpus) DeepCopyInto(out *AgentCorpus) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy returns a deep copy of this AgentCorpus. -func (in *AgentCorpus) DeepCopy() *AgentCorpus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCollective. +func (in *AgentCollective) DeepCopy() *AgentCollective { if in == nil { return nil } - out := new(AgentCorpus) + out := new(AgentCollective) in.DeepCopyInto(out) return out } -// DeepCopyObject implements runtime.Object. -func (in *AgentCorpus) DeepCopyObject() runtime.Object { +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentCollective) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } -// DeepCopyInto copies all properties of AgentCorpusList. -func (in *AgentCorpusList) DeepCopyInto(out *AgentCorpusList) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCollectiveList) DeepCopyInto(out *AgentCollectiveList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentCorpus, len(*in)) + *out = make([]AgentCollective, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy returns a deep copy of AgentCorpusList. -func (in *AgentCorpusList) DeepCopy() *AgentCorpusList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCollectiveList. +func (in *AgentCollectiveList) DeepCopy() *AgentCollectiveList { if in == nil { return nil } - out := new(AgentCorpusList) + out := new(AgentCollectiveList) in.DeepCopyInto(out) return out } -// DeepCopyObject implements runtime.Object. -func (in *AgentCorpusList) DeepCopyObject() runtime.Object { +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentCollectiveList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } -// DeepCopyInto copies AgentCorpusSpec. -func (in *AgentCorpusSpec) DeepCopyInto(out *AgentCorpusSpec) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCollectiveSpec) DeepCopyInto(out *AgentCollectiveSpec) { *out = *in - if in.Collectives != nil { - in, out := &in.Collectives, &out.Collectives - *out = make([]CollectiveRef, len(*in)) - copy(*out, *in) + out.CorpusRef = in.CorpusRef + if in.Agents != nil { + in, out := &in.Agents, &out.Agents + *out = make([]AgentRoleSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - in.Infrastructure.DeepCopyInto(&out.Infrastructure) - in.Governance.DeepCopyInto(&out.Governance) - if in.Kafka != nil { - in, out := &in.Kafka, &out.Kafka - *out = new(KafkaSpec) + in.LLM.DeepCopyInto(&out.LLM) + if in.Scaling != nil { + in, out := &in.Scaling, &out.Scaling + *out = new(ScalingSpec) (*in).DeepCopyInto(*out) } - in.Observability.DeepCopyInto(&out.Observability) - out.UpgradePolicy = in.UpgradePolicy - if in.Edge != nil { - in, out := &in.Edge, &out.Edge - *out = new(EdgeSpec) - **out = **in + if in.RoleDefinition != nil { + in, out := &in.RoleDefinition, &out.RoleDefinition + *out = new(RoleDefinition) + (*in).DeepCopyInto(*out) } } -// DeepCopyInto copies EdgeSpec (ACC-8). -// All fields are scalars — a shallow copy suffices. -func (in *EdgeSpec) DeepCopyInto(out *EdgeSpec) { - *out = *in -} - -// DeepCopy returns a deep copy of EdgeSpec. -func (in *EdgeSpec) DeepCopy() *EdgeSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCollectiveSpec. +func (in *AgentCollectiveSpec) DeepCopy() *AgentCollectiveSpec { if in == nil { return nil } - out := new(EdgeSpec) + out := new(AgentCollectiveSpec) in.DeepCopyInto(out) return out } -// DeepCopyInto copies InfrastructureSpec. -func (in *InfrastructureSpec) DeepCopyInto(out *InfrastructureSpec) { - *out = *in - in.NATS.DeepCopyInto(&out.NATS) - in.Redis.DeepCopyInto(&out.Redis) - if in.Milvus != nil { - in, out := &in.Milvus, &out.Milvus - *out = new(MilvusSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopyInto copies NATSSpec. -func (in *NATSSpec) DeepCopyInto(out *NATSSpec) { - *out = *in - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(corev1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopyInto copies RedisSpec. -func (in *RedisSpec) DeepCopyInto(out *RedisSpec) { - *out = *in - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(corev1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopyInto copies MilvusSpec. -func (in *MilvusSpec) DeepCopyInto(out *MilvusSpec) { - *out = *in - if in.CredentialsSecretRef != nil { - in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef - *out = new(corev1.SecretReference) - **out = **in - } -} - -// DeepCopyInto copies GovernanceSpec. -func (in *GovernanceSpec) DeepCopyInto(out *GovernanceSpec) { - *out = *in - out.CategoryA = in.CategoryA - out.CategoryB = in.CategoryB - if in.CategoryC != nil { - in, out := &in.CategoryC, &out.CategoryC - *out = new(CategoryCSpec) - **out = **in - } -} - -// DeepCopyInto copies KafkaSpec. -func (in *KafkaSpec) DeepCopyInto(out *KafkaSpec) { - *out = *in - if in.CredentialsSecretRef != nil { - in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef - *out = new(corev1.SecretReference) - **out = **in - } -} - -// DeepCopyInto copies ObservabilitySpec. -func (in *ObservabilitySpec) DeepCopyInto(out *ObservabilitySpec) { - *out = *in - if in.OTelCollector != nil { - in, out := &in.OTelCollector, &out.OTelCollector - *out = new(OTelCollectorSpec) - **out = **in - } -} - -// DeepCopyInto copies AgentCorpusStatus. -func (in *AgentCorpusStatus) DeepCopyInto(out *AgentCorpusStatus) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCollectiveStatus) DeepCopyInto(out *AgentCollectiveStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - out.Prerequisites = in.Prerequisites - out.Infrastructure = in.Infrastructure - if in.CollectiveStatuses != nil { - in, out := &in.CollectiveStatuses, &out.CollectiveStatuses - *out = make(map[string]CollectiveStatus, len(*in)) - for k, v := range *in { - (*out)[k] = *v.DeepCopy() - } - } -} - -// DeepCopy returns a deep copy of CollectiveStatus. -func (in *CollectiveStatus) DeepCopy() *CollectiveStatus { - if in == nil { - return nil - } - out := new(CollectiveStatus) - *out = *in if in.ReadyAgents != nil { in, out := &in.ReadyAgents, &out.ReadyAgents *out = make(map[string]int32, len(*in)) - for k, v := range *in { - (*out)[k] = v + for key, val := range *in { + (*out)[key] = val } } if in.DesiredAgents != nil { in, out := &in.DesiredAgents, &out.DesiredAgents *out = make(map[string]int32, len(*in)) - for k, v := range *in { - (*out)[k] = v + for key, val := range *in { + (*out)[key] = val } } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCollectiveStatus. +func (in *AgentCollectiveStatus) DeepCopy() *AgentCollectiveStatus { + if in == nil { + return nil } + out := new(AgentCollectiveStatus) + in.DeepCopyInto(out) return out } -// ----------------------------------------------------------------------- -// AgentCollective -// ----------------------------------------------------------------------- - -// DeepCopyInto copies AgentCollective. -func (in *AgentCollective) DeepCopyInto(out *AgentCollective) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCorpus) DeepCopyInto(out *AgentCorpus) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -256,145 +162,384 @@ func (in *AgentCollective) DeepCopyInto(out *AgentCollective) { in.Status.DeepCopyInto(&out.Status) } -// DeepCopy returns a deep copy of AgentCollective. -func (in *AgentCollective) DeepCopy() *AgentCollective { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCorpus. +func (in *AgentCorpus) DeepCopy() *AgentCorpus { if in == nil { return nil } - out := new(AgentCollective) + out := new(AgentCorpus) in.DeepCopyInto(out) return out } -// DeepCopyObject implements runtime.Object. -func (in *AgentCollective) DeepCopyObject() runtime.Object { +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentCorpus) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } -// DeepCopyInto copies AgentCollectiveList. -func (in *AgentCollectiveList) DeepCopyInto(out *AgentCollectiveList) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCorpusList) DeepCopyInto(out *AgentCorpusList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]AgentCollective, len(*in)) + *out = make([]AgentCorpus, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy returns a deep copy of AgentCollectiveList. -func (in *AgentCollectiveList) DeepCopy() *AgentCollectiveList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCorpusList. +func (in *AgentCorpusList) DeepCopy() *AgentCorpusList { if in == nil { return nil } - out := new(AgentCollectiveList) + out := new(AgentCorpusList) in.DeepCopyInto(out) return out } -// DeepCopyObject implements runtime.Object. -func (in *AgentCollectiveList) DeepCopyObject() runtime.Object { +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AgentCorpusList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } -// DeepCopyInto copies AgentCollectiveSpec. -func (in *AgentCollectiveSpec) DeepCopyInto(out *AgentCollectiveSpec) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCorpusSpec) DeepCopyInto(out *AgentCorpusSpec) { *out = *in - out.CorpusRef = in.CorpusRef - if in.Agents != nil { - in, out := &in.Agents, &out.Agents - *out = make([]AgentRoleSpec, len(*in)) + if in.Collectives != nil { + in, out := &in.Collectives, &out.Collectives + *out = make([]CollectiveRef, len(*in)) + copy(*out, *in) + } + in.Infrastructure.DeepCopyInto(&out.Infrastructure) + in.Governance.DeepCopyInto(&out.Governance) + if in.Kafka != nil { + in, out := &in.Kafka, &out.Kafka + *out = new(KafkaSpec) + (*in).DeepCopyInto(*out) + } + in.Observability.DeepCopyInto(&out.Observability) + out.UpgradePolicy = in.UpgradePolicy + if in.Edge != nil { + in, out := &in.Edge, &out.Edge + *out = new(EdgeSpec) + **out = **in + } + if in.MCPServers != nil { + in, out := &in.MCPServers, &out.MCPServers + *out = make([]MCPServerSpec, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.LLM.DeepCopyInto(&out.LLM) - if in.Scaling != nil { - in, out := &in.Scaling, &out.Scaling - *out = new(ScalingSpec) - (*in).DeepCopyInto(*out) - } - if in.RoleDefinition != nil { - in, out := &in.RoleDefinition, &out.RoleDefinition - *out = new(RoleDefinition) - (*in).DeepCopyInto(*out) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCorpusSpec. +func (in *AgentCorpusSpec) DeepCopy() *AgentCorpusSpec { + if in == nil { + return nil } + out := new(AgentCorpusSpec) + in.DeepCopyInto(out) + return out } -// DeepCopyInto copies RoleDefinition. -func (in *RoleDefinition) DeepCopyInto(out *RoleDefinition) { +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentCorpusStatus) DeepCopyInto(out *AgentCorpusStatus) { *out = *in - if in.TaskTypes != nil { - in, out := &in.TaskTypes, &out.TaskTypes - *out = make([]string, len(*in)) - copy(*out, *in) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } - if in.AllowedActions != nil { - in, out := &in.AllowedActions, &out.AllowedActions - *out = make([]string, len(*in)) - copy(*out, *in) + out.Prerequisites = in.Prerequisites + out.Infrastructure = in.Infrastructure + if in.CollectiveStatuses != nil { + in, out := &in.CollectiveStatuses, &out.CollectiveStatuses + *out = make(map[string]CollectiveStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } } - if in.CategoryBOverrides != nil { - in, out := &in.CategoryBOverrides, &out.CategoryBOverrides - *out = make(map[string]string, len(*in)) + if in.MCPServerStatuses != nil { + in, out := &in.MCPServerStatuses, &out.MCPServerStatuses + *out = make(map[string]MCPServerStatus, len(*in)) for key, val := range *in { (*out)[key] = val } } } -// DeepCopy returns a deep copy of RoleDefinition. -func (in *RoleDefinition) DeepCopy() *RoleDefinition { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentCorpusStatus. +func (in *AgentCorpusStatus) DeepCopy() *AgentCorpusStatus { if in == nil { return nil } - out := new(RoleDefinition) + out := new(AgentCorpusStatus) in.DeepCopyInto(out) return out } -// DeepCopyInto copies AgentRoleSpec. +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AgentRoleSpec) DeepCopyInto(out *AgentRoleSpec) { *out = *in if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(corev1.ResourceRequirements) + *out = new(v1.ResourceRequirements) (*in).DeepCopyInto(*out) } if in.ExtraEnv != nil { in, out := &in.ExtraEnv, &out.ExtraEnv - *out = make([]corev1.EnvVar, len(*in)) + *out = make([]v1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } if in.VolumeClaimTemplates != nil { in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates - *out = make([]corev1.PersistentVolumeClaimTemplate, len(*in)) + *out = make([]v1.PersistentVolumeClaimTemplate, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopyInto copies LLMSpec. -func (in *LLMSpec) DeepCopyInto(out *LLMSpec) { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentRoleSpec. +func (in *AgentRoleSpec) DeepCopy() *AgentRoleSpec { + if in == nil { + return nil + } + out := new(AgentRoleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnthropicSpec) DeepCopyInto(out *AnthropicSpec) { *out = *in - if in.Ollama != nil { - in, out := &in.Ollama, &out.Ollama - *out = new(OllamaSpec) - **out = **in + in.APIKeySecretRef.DeepCopyInto(&out.APIKeySecretRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnthropicSpec. +func (in *AnthropicSpec) DeepCopy() *AnthropicSpec { + if in == nil { + return nil } - if in.Anthropic != nil { + out := new(AnthropicSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CategoryASpec) DeepCopyInto(out *CategoryASpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CategoryASpec. +func (in *CategoryASpec) DeepCopy() *CategoryASpec { + if in == nil { + return nil + } + out := new(CategoryASpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CategoryBSpec) DeepCopyInto(out *CategoryBSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CategoryBSpec. +func (in *CategoryBSpec) DeepCopy() *CategoryBSpec { + if in == nil { + return nil + } + out := new(CategoryBSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CategoryCSpec) DeepCopyInto(out *CategoryCSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CategoryCSpec. +func (in *CategoryCSpec) DeepCopy() *CategoryCSpec { + if in == nil { + return nil + } + out := new(CategoryCSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CollectiveRef) DeepCopyInto(out *CollectiveRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectiveRef. +func (in *CollectiveRef) DeepCopy() *CollectiveRef { + if in == nil { + return nil + } + out := new(CollectiveRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CollectiveStatus) DeepCopyInto(out *CollectiveStatus) { + *out = *in + if in.ReadyAgents != nil { + in, out := &in.ReadyAgents, &out.ReadyAgents + *out = make(map[string]int32, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.DesiredAgents != nil { + in, out := &in.DesiredAgents, &out.DesiredAgents + *out = make(map[string]int32, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CollectiveStatus. +func (in *CollectiveStatus) DeepCopy() *CollectiveStatus { + if in == nil { + return nil + } + out := new(CollectiveStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EdgeSpec) DeepCopyInto(out *EdgeSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EdgeSpec. +func (in *EdgeSpec) DeepCopy() *EdgeSpec { + if in == nil { + return nil + } + out := new(EdgeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GovernanceSpec) DeepCopyInto(out *GovernanceSpec) { + *out = *in + out.CategoryA = in.CategoryA + out.CategoryB = in.CategoryB + if in.CategoryC != nil { + in, out := &in.CategoryC, &out.CategoryC + *out = new(CategoryCSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GovernanceSpec. +func (in *GovernanceSpec) DeepCopy() *GovernanceSpec { + if in == nil { + return nil + } + out := new(GovernanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureSpec) DeepCopyInto(out *InfrastructureSpec) { + *out = *in + in.NATS.DeepCopyInto(&out.NATS) + in.Redis.DeepCopyInto(&out.Redis) + if in.Milvus != nil { + in, out := &in.Milvus, &out.Milvus + *out = new(MilvusSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureSpec. +func (in *InfrastructureSpec) DeepCopy() *InfrastructureSpec { + if in == nil { + return nil + } + out := new(InfrastructureSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureStatus) DeepCopyInto(out *InfrastructureStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureStatus. +func (in *InfrastructureStatus) DeepCopy() *InfrastructureStatus { + if in == nil { + return nil + } + out := new(InfrastructureStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaSpec) DeepCopyInto(out *KafkaSpec) { + *out = *in + if in.CredentialsSecretRef != nil { + in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaSpec. +func (in *KafkaSpec) DeepCopy() *KafkaSpec { + if in == nil { + return nil + } + out := new(KafkaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LLMSpec) DeepCopyInto(out *LLMSpec) { + *out = *in + if in.Ollama != nil { + in, out := &in.Ollama, &out.Ollama + *out = new(OllamaSpec) + **out = **in + } + if in.Anthropic != nil { in, out := &in.Anthropic, &out.Anthropic *out = new(AnthropicSpec) (*in).DeepCopyInto(*out) @@ -411,23 +556,253 @@ func (in *LLMSpec) DeepCopyInto(out *LLMSpec) { } } -// DeepCopyInto copies AnthropicSpec. -func (in *AnthropicSpec) DeepCopyInto(out *AnthropicSpec) { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LLMSpec. +func (in *LLMSpec) DeepCopy() *LLMSpec { + if in == nil { + return nil + } + out := new(LLMSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LlamaStackSpec) DeepCopyInto(out *LlamaStackSpec) { *out = *in - out.APIKeySecretRef = in.APIKeySecretRef } -// DeepCopyInto copies VLLMSpec. -func (in *VLLMSpec) DeepCopyInto(out *VLLMSpec) { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LlamaStackSpec. +func (in *LlamaStackSpec) DeepCopy() *LlamaStackSpec { + if in == nil { + return nil + } + out := new(LlamaStackSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MCPServerSpec) DeepCopyInto(out *MCPServerSpec) { + *out = *in + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SecretEnv != nil { + in, out := &in.SecretEnv, &out.SecretEnv + *out = make([]v1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPServerSpec. +func (in *MCPServerSpec) DeepCopy() *MCPServerSpec { + if in == nil { + return nil + } + out := new(MCPServerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MCPServerStatus) DeepCopyInto(out *MCPServerStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MCPServerStatus. +func (in *MCPServerStatus) DeepCopy() *MCPServerStatus { + if in == nil { + return nil + } + out := new(MCPServerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MilvusSpec) DeepCopyInto(out *MilvusSpec) { + *out = *in + if in.CredentialsSecretRef != nil { + in, out := &in.CredentialsSecretRef, &out.CredentialsSecretRef + *out = new(v1.SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MilvusSpec. +func (in *MilvusSpec) DeepCopy() *MilvusSpec { + if in == nil { + return nil + } + out := new(MilvusSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NATSSpec) DeepCopyInto(out *NATSSpec) { *out = *in if in.Resources != nil { in, out := &in.Resources, &out.Resources - *out = new(corev1.ResourceRequirements) + *out = new(v1.ResourceRequirements) (*in).DeepCopyInto(*out) } } -// DeepCopyInto copies ScalingSpec. +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NATSSpec. +func (in *NATSSpec) DeepCopy() *NATSSpec { + if in == nil { + return nil + } + out := new(NATSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OTelCollectorSpec) DeepCopyInto(out *OTelCollectorSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OTelCollectorSpec. +func (in *OTelCollectorSpec) DeepCopy() *OTelCollectorSpec { + if in == nil { + return nil + } + out := new(OTelCollectorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilitySpec) DeepCopyInto(out *ObservabilitySpec) { + *out = *in + if in.OTelCollector != nil { + in, out := &in.OTelCollector, &out.OTelCollector + *out = new(OTelCollectorSpec) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilitySpec. +func (in *ObservabilitySpec) DeepCopy() *ObservabilitySpec { + if in == nil { + return nil + } + out := new(ObservabilitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OllamaSpec) DeepCopyInto(out *OllamaSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OllamaSpec. +func (in *OllamaSpec) DeepCopy() *OllamaSpec { + if in == nil { + return nil + } + out := new(OllamaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrerequisiteStatus) DeepCopyInto(out *PrerequisiteStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrerequisiteStatus. +func (in *PrerequisiteStatus) DeepCopy() *PrerequisiteStatus { + if in == nil { + return nil + } + out := new(PrerequisiteStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSpec) DeepCopyInto(out *RedisSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSpec. +func (in *RedisSpec) DeepCopy() *RedisSpec { + if in == nil { + return nil + } + out := new(RedisSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleDefinition) DeepCopyInto(out *RoleDefinition) { + *out = *in + if in.TaskTypes != nil { + in, out := &in.TaskTypes, &out.TaskTypes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllowedActions != nil { + in, out := &in.AllowedActions, &out.AllowedActions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CategoryBOverrides != nil { + in, out := &in.CategoryBOverrides, &out.CategoryBOverrides + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleDefinition. +func (in *RoleDefinition) DeepCopy() *RoleDefinition { + if in == nil { + return nil + } + out := new(RoleDefinition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleScalingSpec) DeepCopyInto(out *RoleScalingSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleScalingSpec. +func (in *RoleScalingSpec) DeepCopy() *RoleScalingSpec { + if in == nil { + return nil + } + out := new(RoleScalingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScalingSpec) DeepCopyInto(out *ScalingSpec) { *out = *in if in.RoleScaling != nil { @@ -437,28 +812,47 @@ func (in *ScalingSpec) DeepCopyInto(out *ScalingSpec) { } } -// DeepCopyInto copies AgentCollectiveStatus. -func (in *AgentCollectiveStatus) DeepCopyInto(out *AgentCollectiveStatus) { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScalingSpec. +func (in *ScalingSpec) DeepCopy() *ScalingSpec { + if in == nil { + return nil + } + out := new(ScalingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradePolicySpec) DeepCopyInto(out *UpgradePolicySpec) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradePolicySpec. +func (in *UpgradePolicySpec) DeepCopy() *UpgradePolicySpec { + if in == nil { + return nil } - if in.ReadyAgents != nil { - in, out := &in.ReadyAgents, &out.ReadyAgents - *out = make(map[string]int32, len(*in)) - for k, v := range *in { - (*out)[k] = v - } + out := new(UpgradePolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VLLMSpec) DeepCopyInto(out *VLLMSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) } - if in.DesiredAgents != nil { - in, out := &in.DesiredAgents, &out.DesiredAgents - *out = make(map[string]int32, len(*in)) - for k, v := range *in { - (*out)[k] = v - } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLLMSpec. +func (in *VLLMSpec) DeepCopy() *VLLMSpec { + if in == nil { + return nil } + out := new(VLLMSpec) + in.DeepCopyInto(out) + return out } diff --git a/operator/config/crd/bases/acc.redhat.io_agentcollectives.yaml b/operator/config/crd/bases/acc.redhat.io_agentcollectives.yaml index 68cc1143..4e7562ed 100644 --- a/operator/config/crd/bases/acc.redhat.io_agentcollectives.yaml +++ b/operator/config/crd/bases/acc.redhat.io_agentcollectives.yaml @@ -34,62 +34,153 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: AgentCollective represents a single collective within an AgentCorpus. + description: |- + AgentCollective represents a single collective within an AgentCorpus. + It is namespace-scoped and owned by its parent AgentCorpus. properties: apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: - description: AgentCollectiveSpec defines a single collective within the corpus. + description: AgentCollectiveSpec defines a single collective within the + corpus. properties: agents: description: Agents lists the role-specific deployment configurations. items: + description: AgentRoleSpec defines the deployment configuration + for one agent role. properties: extraEnv: + description: ExtraEnv injects additional environment variables + into agent pods. items: - description: EnvVar represents an environment variable present in a Container. + description: EnvVar represents an environment variable present + in a Container. properties: name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. type: string value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". type: string valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. properties: configMapKeyRef: + description: Selects a key of a ConfigMap. properties: key: + description: The key to select. type: string name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: + description: Specify whether the ConfigMap or + its key must be defined type: boolean required: - key type: object + x-kubernetes-map-type: atomic fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. properties: apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". type: string fieldPath: + description: Path of the field to select in the + specified API version. type: string required: - fieldPath type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic secretKeyRef: + description: Selects a key of a secret in the pod's + namespace properties: key: + description: The key of the secret to select from. Must + be a valid secret key. type: string name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: + description: Specify whether the Secret or its + key must be defined type: boolean required: - key type: object + x-kubernetes-map-type: atomic type: object required: - name @@ -97,13 +188,47 @@ spec: type: array replicas: default: 1 + description: Replicas is the baseline replica count (before + KEDA scaling). format: int32 maximum: 100 minimum: 0 type: integer resources: - description: ResourceRequirements describes the compute resource requirements. + description: Resources sets the CPU/memory requests and limits + for agent pods. properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map limits: additionalProperties: anyOf: @@ -111,6 +236,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -119,34 +247,279 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object role: - description: Role identifies the ACC agent role. - enum: - - ingester - - analyst - - synthesizer - - arbiter - - observer + description: |- + Role identifies the ACC agent role. Schema-level validation is the + regex pattern on the AgentRole type; semantic validation against the + operator's compiled-in catalogue happens in the AgentCollective webhook. + maxLength: 63 + minLength: 2 + pattern: ^[a-z][a-z0-9_]{1,62}$ type: string + volumeClaimTemplates: + description: VolumeClaimTemplates allows attaching role-specific + PVCs. + items: + description: |- + PersistentVolumeClaimTemplate is used to produce + PersistentVolumeClaim objects as part of an EphemeralVolumeSource. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being + referenced + type: string + name: + description: Name is the name of resource being + referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being + referenced + type: string + name: + description: Name is the name of resource being + referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes + to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ + (Beta) Using this field requires the VolumeAttributesClass feature gate to be enabled (off by default). + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to + the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: array required: - role type: object minItems: 1 type: array collectiveId: - description: CollectiveID is the logical identifier used in NATS subjects. + description: |- + CollectiveID is the logical identifier used in NATS subjects + (acc.{collectiveID}.{signal_type}) and in agent labels. + Must be DNS-label-safe. pattern: ^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$ type: string corpusRef: - description: CorpusRef names the AgentCorpus that owns this collective. + description: |- + CorpusRef names the AgentCorpus that owns this collective. + Set automatically by the AgentCorpus reconciler; do not set manually. properties: name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string type: object + x-kubernetes-map-type: atomic heartbeatIntervalSeconds: default: 30 + description: |- + HeartbeatIntervalSeconds sets the HEARTBEAT emission interval for all agents. + Matches acc-config.yaml agent.heartbeat_interval_s. format: int32 maximum: 300 minimum: 5 @@ -155,71 +528,148 @@ spec: description: LLM configures the language model backend for this collective. properties: anthropic: + description: Anthropic configures the Anthropic Claude API backend. properties: apiKeySecretRef: + description: APIKeySecretRef references a Secret with an ACC_ANTHROPIC_API_KEY + key. properties: key: + description: The key of the secret to select from. Must + be a valid secret key. type: string name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names type: string optional: + description: Specify whether the Secret or its key must + be defined type: boolean required: - key type: object + x-kubernetes-map-type: atomic model: default: claude-sonnet-4-6 + description: Model is the Anthropic model identifier. type: string required: - apiKeySecretRef - model type: object backend: + allOf: + - enum: + - ollama + - anthropic + - vllm + - llama_stack + - enum: + - ollama + - anthropic + - vllm + - llama_stack description: Backend selects the LLM implementation. - enum: - - ollama - - anthropic - - vllm - - llama_stack type: string embeddingModel: default: all-MiniLM-L6-v2 + description: EmbeddingModel is the sentence-transformers model + for local embedding fallback. type: string llamaStack: + description: LlamaStack configures the Llama Stack inference API + (rhoai mode). properties: baseUrl: + description: BaseURL is the Llama Stack distribution endpoint. type: string modelId: + description: ModelID is the Llama Stack model identifier. type: string required: - baseUrl - modelId type: object ollama: + description: Ollama configures the Ollama REST backend (standalone + mode). properties: baseUrl: default: http://localhost:11434 + description: BaseURL is the Ollama REST API endpoint. type: string model: default: llama3.2:3b + description: Model is the Ollama model identifier. type: string required: - baseUrl - model type: object vllm: + description: VLLM configures the KServe InferenceService backend + (rhoai mode). properties: deploy: default: true + description: |- + Deploy controls whether the operator creates the InferenceService. + Set to false to manage the InferenceService separately. type: boolean inferenceServiceRef: + description: |- + InferenceServiceRef names the KServe InferenceService in the same namespace. + The operator reads the InferenceService URL from its status. type: string model: + description: Model is the vLLM model identifier. type: string modelStoragePVC: + description: |- + ModelStoragePVC is the PVC name containing the model weights. + Required when deploy=true. type: string resources: + description: Resources for the InferenceService predictor + pod. properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map limits: additionalProperties: anyOf: @@ -227,6 +677,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -235,6 +688,11 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object required: @@ -244,43 +702,106 @@ spec: required: - backend type: object + roleDefinition: + description: |- + RoleDefinition sets the cognitive role for all agents in this collective. + The reconciler renders this into a ConfigMap named acc-role-{collectiveId} + mounted read-only into every agent pod at /app/acc-role.yaml. + Agents load this as the highest-priority role source on startup. + properties: + allowedActions: + description: AllowedActions lists the actions the agent may perform. + items: + type: string + type: array + categoryBOverrides: + additionalProperties: + type: string + description: |- + CategoryBOverrides are live-updatable Cat-B governance setpoints + (e.g. token_budget, rate_limit_rpm). + type: object + persona: + default: concise + description: Persona controls the LLM response style. + enum: + - concise + - formal + - exploratory + - analytical + type: string + purpose: + description: |- + Purpose is the agent's primary objective statement, injected as the + first component of the CognitiveCore system prompt. + type: string + seedContext: + description: SeedContext is a domain-specific priming string injected + into every LLM call. + type: string + taskTypes: + description: TaskTypes lists the NATS signal types this agent + will accept. + items: + type: string + type: array + version: + default: 0.1.0 + description: Version is the semantic version of this role definition. + type: string + type: object scaling: - description: Scaling configures KEDA-based autoscaling per role. + description: |- + Scaling configures KEDA-based autoscaling per role. + Requires KEDA to be installed cluster-wide (operator checks via API discovery). properties: enabled: default: false + description: |- + Enabled activates KEDA ScaledObjects for all roles in this collective. + Requires KEDA to be installed cluster-wide. type: boolean roleScaling: + description: RoleScaling allows per-role scaling overrides. items: + description: RoleScalingSpec configures KEDA scaling for a single + agent role. properties: healthMetricThreshold: default: 70 + description: |- + HealthMetricThreshold scales down when the role's average health_score + drops below this value (percentage 0-100). format: int32 maximum: 100 minimum: 0 type: integer maxReplicas: default: 10 + description: MaxReplicas is the KEDA maximum replica count. format: int32 minimum: 1 type: integer minReplicas: default: 1 + description: MinReplicas is the KEDA minimum replica count. format: int32 minimum: 0 type: integer natsConsumerLagThreshold: default: 10 + description: NATSConsumerLagThreshold scales up when NATS + consumer lag exceeds this value. format: int64 minimum: 1 type: integer role: - enum: - - ingester - - analyst - - synthesizer - - arbiter - - observer + description: |- + Role identifies which agent role this scaling config applies to. + Validation: regex on the AgentRole type + webhook catalogue check. + maxLength: 63 + minLength: 2 + pattern: ^[a-z][a-z0-9_]{1,62}$ type: string required: - role @@ -299,21 +820,53 @@ spec: description: AgentCollectiveStatus reports the observed state of one collective. properties: conditions: + description: Conditions contains the condition set for this collective. items: + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 type: string observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 + minimum: 0 type: integer reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime @@ -330,20 +883,34 @@ spec: additionalProperties: format: int32 type: integer + description: DesiredAgents maps role → desired replica count. type: object kserveReady: + description: KServeReady is true when the InferenceService is in ready + state. type: boolean observedGeneration: + description: ObservedGeneration is the .metadata.generation this status + reflects. format: int64 type: integer phase: + description: Phase is the collective's operational state. + enum: + - Pending + - Progressing + - Ready + - Degraded type: string readyAgents: additionalProperties: format: int32 type: integer + description: ReadyAgents maps role → number of ready pods. type: object scaledObjectsActive: + description: ScaledObjectsActive is true when KEDA ScaledObjects are + in effect. type: boolean type: object type: object diff --git a/operator/config/crd/bases/acc.redhat.io_agentcorpora.yaml b/operator/config/crd/bases/acc.redhat.io_agentcorpora.yaml index 197889d1..df9840e0 100644 --- a/operator/config/crd/bases/acc.redhat.io_agentcorpora.yaml +++ b/operator/config/crd/bases/acc.redhat.io_agentcorpora.yaml @@ -37,8 +37,19 @@ spec: description: AgentCorpus is the primary resource representing a full ACC deployment. properties: apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -46,10 +57,16 @@ spec: description: AgentCorpusSpec defines the desired state of an ACC deployment. properties: collectives: - description: Collectives lists references to AgentCollective resources this corpus manages. + description: |- + Collectives lists references to AgentCollective resources this corpus manages. + Each collective gets its own set of agent Deployments and ScaledObjects. items: + description: CollectiveRef references an AgentCollective resource + in the same namespace. properties: name: + description: Name is the metadata.name of the AgentCollective + resource. maxLength: 63 minLength: 1 type: string @@ -60,52 +77,128 @@ spec: minItems: 1 type: array deployMode: + allOf: + - enum: + - standalone + - rhoai + - edge + - enum: + - standalone + - rhoai + - edge default: standalone - description: DeployMode selects the infrastructure profile. - enum: - - standalone - - rhoai + description: |- + DeployMode selects the infrastructure profile. + "standalone" — local dev / CI / Podman Compose. + "edge" — MicroShift / K3s / production edge node; NATS runs as a leaf node + connecting to a datacenter hub when network is available. + "rhoai" — OpenShift datacenter with RHOAI GPU inference. type: string + edge: + description: |- + Edge configures edge-specific behaviour (deployMode=edge only). + Ignored when deployMode is standalone or rhoai. + properties: + hubCollectiveId: + description: |- + HubCollectiveID is the collective ID of the datacenter hub that edge + agents should delegate complex tasks to (ACC-9 bridge protocol). + Rendered as ACC_HUB_COLLECTIVE_ID in agent pod environments. + type: string + hubNatsUrl: + description: |- + HubNatsUrl is the NATS leaf node remote URL of the datacenter hub. + When set, the operator renders a leafnodes block in nats.conf so the + local NATS server connects to the hub and forwards bridge subjects. + Format: nats-leaf://hub.example.com:7422 + type: string + hubRegistry: + description: |- + HubRegistry is the container image registry to pull from when the + edge node has connectivity to the hub (image pull on reconnect). + Defaults to spec.imageRegistry when empty. + type: string + redisMaxMemoryMb: + default: 512 + description: |- + RedisMaxMemoryMB caps Redis working memory (MiB) to prevent OOM on + edge hardware. When > 0, sets Redis maxmemory and maxmemory-policy. + format: int32 + minimum: 64 + type: integer + redisMaxMemoryPolicy: + default: allkeys-lru + description: RedisMaxMemoryPolicy controls Redis eviction when + maxmemory is reached. + enum: + - allkeys-lru + - allkeys-lfu + - volatile-lru + - noeviction + type: string + type: object governance: description: Governance configures the 3-tier rule system. properties: categoryA: + description: CategoryA configures immutable constitutional rule + enforcement. properties: wasmConfigMapRef: + description: |- + WASMConfigMapRef names the ConfigMap holding the compiled + category_a.wasm blob to mount into each agent pod. minLength: 1 type: string required: - wasmConfigMapRef type: object categoryB: + description: CategoryB configures the live-updatable OPA bundle + server. properties: bundlePVCSize: default: 500Mi + description: BundlePVCSize is the PVC size for the bundle + store. type: string bundleServerImage: default: openpolicyagent/opa:latest + description: BundleServerImage is the OPA bundle server image. type: string pollIntervalSeconds: default: 30 + description: PollIntervalSeconds controls how often agent + OPA sidecars poll the bundle server. + format: int32 maximum: 300 minimum: 10 type: integer - format: int32 type: object categoryC: + description: CategoryC configures adaptive rule generation by + the arbiter. properties: confidenceThreshold: + description: |- + ConfidenceThreshold is the minimum ICL confidence an arbiter must have + before signing a Category C rule. Expressed as a decimal string, e.g. "0.80". pattern: ^0\.[0-9]+$|^1\.0+$ type: string maxRulesPerCollective: default: 100 + description: MaxRulesPerCollective caps the number of active + Cat-C rules. + format: int32 maximum: 1000 minimum: 1 type: integer - format: int32 type: object gatekeeperIntegration: default: false + description: |- + GatekeeperIntegration enables syncing Category A rules as OPA Gatekeeper + ConstraintTemplates (requires Gatekeeper to be installed cluster-wide). type: boolean required: - categoryA @@ -114,39 +207,88 @@ spec: type: object imageRegistry: default: registry.access.redhat.com + description: ImageRegistry is the base registry for acc-agent-core + and infrastructure images. type: string infrastructure: description: Infrastructure configures the shared backing services. properties: milvus: + description: |- + Milvus configures the datacenter vector DB (rhoai mode only). + Required when deployMode=rhoai; ignored in standalone mode. properties: collectionPrefix: default: acc_ - type: string - uri: - minLength: 1 + description: CollectionPrefix is prepended to all ACC Milvus + collection names. type: string credentialsSecretRef: + description: CredentialsSecretRef references a Secret with + milvus_user / milvus_password. properties: name: + description: name is unique within a namespace to reference + a secret resource. type: string namespace: + description: namespace defines the space within which + the secret name must be unique. type: string type: object + x-kubernetes-map-type: atomic + uri: + description: URI is the Milvus gRPC endpoint. + minLength: 1 + type: string required: - uri type: object nats: + description: NATS configures the JetStream StatefulSet. properties: replicas: default: 1 + description: Replicas sets the NATS cluster size (1 = single-node, + 3 = clustered). format: int32 maximum: 5 minimum: 1 type: integer resources: - description: ResourceRequirements describes the compute resource requirements. + description: Resources sets CPU/memory for NATS pods. properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map limits: additionalProperties: anyOf: @@ -154,6 +296,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -162,32 +307,73 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object storageClass: + description: StorageClass for the JetStream PVC. type: string storageSize: default: 2Gi + description: StorageSize for JetStream persistence. type: string version: default: "2.10" + description: Version is the NATS server image tag. pattern: ^[0-9]+\.[0-9]+.*$ type: string required: - - version - replicas + - version type: object redis: + description: Redis configures the working memory store. properties: replicas: default: 1 + description: Replicas (1 = standalone, 3 = Sentinel mode). format: int32 maximum: 6 minimum: 1 type: integer resources: - description: ResourceRequirements describes the compute resource requirements. + description: Resources sets CPU/memory for Redis pods. properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map limits: additionalProperties: anyOf: @@ -195,6 +381,9 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object requests: additionalProperties: @@ -203,98 +392,474 @@ spec: - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object storageSize: default: 1Gi + description: StorageSize for Redis persistence PVC. type: string version: default: "6" + description: Version selects the UBI Redis image tag. type: string required: - - version - replicas + - version type: object required: - nats - redis type: object kafka: + description: |- + Kafka configures the optional NATS-to-Kafka audit bridge. + Kafka itself is NOT installed by the operator. properties: auditTopic: default: acc.audit.all + description: AuditTopic is the Kafka topic for all signal audit + records. type: string bootstrapServers: + description: |- + BootstrapServers is a comma-separated list of Kafka broker addresses. + The operator TCP-probes these to verify connectivity. minLength: 1 type: string bridgeReplicas: default: 1 + description: BridgeReplicas sets the number of bridge Deployment + replicas. format: int32 maximum: 10 minimum: 1 type: integer credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret with kafka_sasl_username / + kafka_sasl_password keys. properties: name: + description: name is unique within a namespace to reference + a secret resource. type: string namespace: + description: namespace defines the space within which the + secret name must be unique. type: string type: object + x-kubernetes-map-type: atomic signalTopicsPrefix: default: acc.signals + description: SignalTopicsPrefix is the prefix for per-signal-type + Kafka topics. type: string required: - bootstrapServers type: object + manifestDelivery: + default: all + description: |- + ManifestDelivery controls how the operator-baked roles/, skills/, and + mcps/ trees reach agent pods. + "all" (default) — operator emits ConfigMaps and mounts them at + /etc/acc/{roles,skills,mcps} in every agent pod. + "none" — operator skips the mounts; users must bake the trees into + a custom agent image. + The reconciler that emits the ConfigMaps and the volume injection in + agent pods both ship in PR-50 (this PR only adds the schema). + enum: + - all + - none + type: string + mcpServers: + description: |- + MCPServers configures shared MCP servers visible to every collective in + this corpus. Each entry produces a Deployment + Service named + acc-mcp-{name}, matching the URL convention used by mcps//mcp.yaml. + The reconciler that owns these objects ships in PR-51 (this PR only adds + the schema). + items: + description: |- + MCPServerSpec configures one MCP server Deployment + Service. + The operator emits a Deployment named acc-mcp-{Name} and a Service of the + same name on Port. The Service name matches the url: field convention used + by mcps//mcp.yaml so agents resolve the server without manifest edits. + + The reconciler that owns these objects ships in PR-51 of the + 20260508-operator-feature-parity-d-e openspec change. + properties: + env: + description: Env injects environment variables into the MCP + container. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + description: |- + Image is the full container image reference for the MCP server, + including registry, repository, and tag. + minLength: 1 + type: string + name: + description: |- + Name matches the directory name under mcps/ in the source tree + (e.g. "web-search-brave"). DNS-label-safe. + maxLength: 63 + minLength: 2 + pattern: ^[a-z][a-z0-9-]{1,62}$ + type: string + port: + default: 8080 + description: |- + Port is the JSON-RPC port the server listens on. Becomes the Service + port and the targetPort. Convention across the bundled MCPs is 8080. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + replicas: + default: 1 + description: Replicas sets the Deployment replica count. + format: int32 + maximum: 10 + minimum: 0 + type: integer + resources: + description: Resources sets CPU/memory requests and limits for + the MCP container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + secretEnv: + description: |- + SecretEnv pulls env vars from referenced Secrets / ConfigMaps via + envFrom (typical use: BRAVE_API_KEY for web-search-brave, an Anthropic + or OpenAI key for web-browser-harness). + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + shmSizeMi: + default: 0 + description: |- + ShmSizeMi mounts a Memory-medium emptyDir at /dev/shm sized to the + requested mebibytes. Required for the browser-harness MCP because + Chromium crashes intermittently with the default 64 MiB tmpfs; + 256 MiB is browser-use's documented minimum. + format: int32 + maximum: 4096 + minimum: 0 + type: integer + required: + - image + - name + type: object + maxItems: 16 + type: array observability: + description: Observability configures OTel collector and Prometheus + rules. properties: backend: + allOf: + - enum: + - log + - otel + - enum: + - log + - otel default: log - enum: - - log - - otel + description: Backend selects the telemetry backend. type: string grafanaDashboard: default: false + description: GrafanaDashboard enables creation of a ConfigMap-based + Grafana dashboard. type: boolean otelCollector: + description: |- + OTelCollector configures the OpenTelemetry collector deployment. + Required when backend=otel. properties: endpoint: + description: Endpoint is the OTLP gRPC/HTTP endpoint to export + telemetry to. minLength: 1 type: string serviceName: default: acc-agent + description: ServiceName is the OTel service.name resource + attribute. type: string tlsInsecure: default: false + description: |- + TLSInsecure disables TLS verification for the remote OTLP endpoint. + Use only in development environments. type: boolean required: - endpoint type: object prometheusRules: default: true + description: PrometheusRules enables creation of PrometheusRule + CRs for ACC alerts. type: boolean required: - backend type: object upgradePolicy: + description: UpgradePolicy controls how the operator handles ACC version + upgrades. properties: minKafkaVersion: + description: |- + MinKafkaVersion is the minimum Kafka broker version required for bridge + compatibility. Operator warns but does not block if Kafka is older. type: string mode: + allOf: + - enum: + - auto + - manual + - enum: + - auto + - manual default: auto - enum: - - auto - - manual + description: Mode selects the upgrade strategy. type: string requireApproval: default: false + description: |- + RequireApproval forces the operator to pause before upgrading shared + infrastructure components (NATS, Redis). When true, the operator writes + a Warning Event and sets UpgradeApprovalPending=True, then waits for the + annotation acc.redhat.io/approve-upgrade= to be applied. type: boolean required: - mode type: object version: default: 0.1.0 + description: |- + Version pins the acc-agent-core image tag to deploy. + Must be a valid SemVer string. pattern: ^v?[0-9]+\.[0-9]+\.[0-9]+.*$ type: string required: @@ -305,27 +870,62 @@ spec: - version type: object status: - description: AgentCorpusStatus reports the observed state of the full ACC deployment. + description: AgentCorpusStatus reports the observed state of the full + ACC deployment. properties: collectiveStatuses: additionalProperties: + description: CollectiveStatus reports per-collective operational + state (embedded in AgentCorpus status). properties: conditions: items: + description: Condition contains details for one aspect of + the current state of this API Resource. properties: lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 type: string observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 + minimum: 0 type: integer reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime @@ -335,9 +935,6 @@ spec: - type type: object type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map desiredAgents: additionalProperties: format: int32 @@ -346,6 +943,13 @@ spec: kserveReady: type: boolean phase: + description: CollectivePhase represents the operational phase + of a single AgentCollective. + enum: + - Pending + - Progressing + - Ready + - Degraded type: string readyAgents: additionalProperties: @@ -355,23 +959,60 @@ spec: scaledObjectsActive: type: boolean type: object + description: CollectiveStatuses maps collective name → its operational + status. type: object conditions: + description: |- + Conditions contains the standard metav1.Condition set. + Known types: Ready, InfrastructureReady, CollectivesReady, + PrerequisitesMet, KafkaBridgeReady, UpgradeApprovalPending. items: + description: Condition contains details for one aspect of the current + state of this API Resource. properties: lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 type: string observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. format: int64 + minimum: 0 type: integer reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown type: string type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime @@ -385,11 +1026,19 @@ spec: - type x-kubernetes-list-type: map currentVersion: + description: CurrentVersion is the ACC version currently deployed. type: string infrastructure: + description: Infrastructure reports the state of operator-managed + shared components. properties: milvusConnected: type: boolean + natsLeafConnected: + description: |- + NATSLeafConnected is true when the edge NATS leaf node has established + a connection to the datacenter hub (deployMode=edge only). + type: boolean natsReady: type: boolean natsVersion: @@ -404,30 +1053,95 @@ spec: type: string type: object kafkaBridgeReady: + description: KafkaBridgeReady is true when the Kafka bridge Deployment + is available. + type: boolean + manifestDeliveryReady: + description: |- + ManifestDeliveryReady is true when the acc-roles, acc-skills, and + acc-mcps ConfigMaps have been emitted by the manifest delivery + reconciler that ships in PR-50. type: boolean + mcpServerStatuses: + additionalProperties: + description: |- + MCPServerStatus reports the operational state of one MCP server. + Aggregated into AgentCorpusStatus.MCPServerStatuses by the MCP reconciler. + properties: + ready: + description: Ready is true when the Deployment's ReadyReplicas + matches Replicas. + type: boolean + replicas: + description: Replicas is the live ReadyReplicas count from the + Deployment. + format: int32 + type: integer + serviceURL: + description: |- + ServiceURL is the in-cluster JSON-RPC URL agents should call. + Format: http://acc-mcp-{name}.{namespace}.svc.cluster.local:{port}/rpc + type: string + required: + - ready + - replicas + type: object + description: |- + MCPServerStatuses reports the per-MCP-server state. Keyed by + MCPServerSpec.Name. Populated by the MCP reconciler that ships in PR-51. + type: object observedGeneration: + description: ObservedGeneration is the .metadata.generation this status + reflects. format: int64 type: integer pendingUpgradeVersion: + description: |- + PendingUpgradeVersion is set when upgradePolicy.requireApproval=true and a + version change is pending user approval. type: string phase: - description: CorpusPhase represents the top-level operational phase. + description: Phase is the top-level operational state. + enum: + - Pending + - Progressing + - Ready + - Degraded + - Error + - UpgradeApprovalPending type: string prerequisites: + description: |- + Prerequisites reports the detected presence of each optional cluster dependency. + Written by the PrerequisiteReconciler; read by downstream sub-reconcilers. properties: allMet: + description: AllMet is true when every required prerequisite is + satisfied. type: boolean gatekeeperInstalled: + description: GatekeeperInstalled is true when templates.gatekeeper.sh + is registered. type: boolean kafkaReachable: + description: KafkaReachable is true when the configured Kafka + bootstrap servers are TCP-reachable. type: boolean kedaInstalled: + description: KEDAInstalled is true when the KEDA API group (keda.sh) + is present. type: boolean kserveInstalled: + description: KServeInstalled is true when serving.kserve.io is + registered. type: boolean prometheusRulesSupported: + description: PrometheusRulesSupported is true when monitoring.coreos.com + is registered. type: boolean rhoaiInstalled: + description: RHOAIInstalled is true when the ODH/RHOAI API group + is registered. type: boolean type: object type: object diff --git a/operator/config/webhook/manifests.yaml b/operator/config/webhook/manifests.yaml index 86ca3d80..aa230325 100644 --- a/operator/config/webhook/manifests.yaml +++ b/operator/config/webhook/manifests.yaml @@ -2,13 +2,33 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - name: acc-operator-mutating-webhook-configuration + name: mutating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: - name: acc-operator-webhook-service + name: webhook-service + namespace: system + path: /mutate-acc-redhat-io-v1alpha1-agentcollective + failurePolicy: Fail + name: magentcollective.kb.io + rules: + - apiGroups: + - acc.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - agentcollectives + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service namespace: system path: /mutate-acc-redhat-io-v1alpha1-agentcorpus failurePolicy: Fail @@ -28,13 +48,33 @@ webhooks: apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: - name: acc-operator-validating-webhook-configuration + name: validating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: - name: acc-operator-webhook-service + name: webhook-service + namespace: system + path: /validate-acc-redhat-io-v1alpha1-agentcollective + failurePolicy: Fail + name: vagentcollective.kb.io + rules: + - apiGroups: + - acc.redhat.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - agentcollectives + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service namespace: system path: /validate-acc-redhat-io-v1alpha1-agentcorpus failurePolicy: Fail diff --git a/operator/go.mod b/operator/go.mod index 3a942254..d1401cce 100644 --- a/operator/go.mod +++ b/operator/go.mod @@ -1,14 +1,16 @@ module github.com/redhat-ai-dev/agentic-cell-corpus/operator -go 1.22 +go 1.23.0 + +toolchain go1.24.6 require ( + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.0 - sigs.k8s.io/yaml v1.4.0 - golang.org/x/mod v0.20.0 ) require ( @@ -27,6 +29,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -34,6 +37,7 @@ require ( github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -50,21 +54,18 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect 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/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.31.0 // indirect - k8s.io/component-base v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240813175640-2e4dc7204e66 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/operator/go.sum b/operator/go.sum new file mode 100644 index 00000000..08350e7b --- /dev/null +++ b/operator/go.sum @@ -0,0 +1,187 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/operator/hack/gen-catalogue.go b/operator/hack/gen-catalogue.go new file mode 100644 index 00000000..cdf2eb2d --- /dev/null +++ b/operator/hack/gen-catalogue.go @@ -0,0 +1,135 @@ +// Copyright 2026 ACC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +//go:build ignore + +// Command gen-catalogue regenerates +// operator/internal/rolecatalogue/known_roles.txt from the source +// repository's roles/ directory. +// +// Run from the operator/ subdirectory: +// +// go run ./hack/gen-catalogue.go +// +// Or via go generate from the catalogue package: +// +// go generate ./internal/rolecatalogue/... +// +// The generator scans /roles/, treats every immediate +// subdirectory that contains a role.yaml as a known role, and skips +// reserved names (_base, TEMPLATE). Output lines are alphabetised so the +// resulting diff is stable when roles are added or removed. +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// outRel is where the generator writes the catalogue, relative to the +// operator/ directory. +const outRel = "internal/rolecatalogue/known_roles.txt" + +var skipDirs = map[string]struct{}{ + "_base": {}, + "TEMPLATE": {}, +} + +func main() { + cwd, err := os.Getwd() + if err != nil { + fail(err) + } + operatorDir, err := findOperatorRoot(cwd) + if err != nil { + fail(err) + } + repoRoot := filepath.Dir(operatorDir) + rolesDir := filepath.Join(repoRoot, "roles") + outPath := filepath.Join(operatorDir, outRel) + + entries, err := os.ReadDir(rolesDir) + if err != nil { + fail(fmt.Errorf("read %s: %w", rolesDir, err)) + } + + var roles []string + for _, e := range entries { + if !e.IsDir() { + continue + } + name := e.Name() + if _, skip := skipDirs[name]; skip { + continue + } + roleFile := filepath.Join(rolesDir, name, "role.yaml") + if _, err := os.Stat(roleFile); err != nil { + continue + } + roles = append(roles, name) + } + sort.Strings(roles) + + var b strings.Builder + b.WriteString("# Generated from roles/ — do not edit by hand.\n") + b.WriteString("# Regenerate via: go run ./hack/gen-catalogue.go\n") + b.WriteString("# (or: go generate ./internal/rolecatalogue/...)\n") + b.WriteString("#\n") + b.WriteString("# Source of truth: roles//role.yaml on the main branch.\n") + b.WriteString("# One role name per line, alphabetical.\n") + b.WriteString("\n") + for _, r := range roles { + b.WriteString(r) + b.WriteString("\n") + } + + if err := os.WriteFile(outPath, []byte(b.String()), 0o644); err != nil { + fail(fmt.Errorf("write %s: %w", outPath, err)) + } + + fmt.Fprintf(os.Stderr, "wrote %d roles to %s\n", len(roles), outPath) +} + +// findOperatorRoot walks up from start until it finds a directory whose +// basename is "operator" and which contains a Makefile + an internal/ +// subdirectory. This makes the generator portable across invocation sites: +// it works whether run from operator/ (`go run ./hack/gen-catalogue.go`) +// or from operator/internal/rolecatalogue/ (`go generate`). +func findOperatorRoot(start string) (string, error) { + dir := start + for { + if filepath.Base(dir) == "operator" { + if isFile(filepath.Join(dir, "Makefile")) && isDir(filepath.Join(dir, "internal")) { + return dir, nil + } + } + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf("could not locate operator/ root above %s", start) + } + dir = parent + } +} + +func isFile(p string) bool { + info, err := os.Stat(p) + return err == nil && !info.IsDir() +} + +func isDir(p string) bool { + info, err := os.Stat(p) + return err == nil && info.IsDir() +} + +func fail(err error) { + fmt.Fprintln(os.Stderr, "gen-catalogue:", err) + os.Exit(1) +} diff --git a/operator/internal/rolecatalogue/catalogue.go b/operator/internal/rolecatalogue/catalogue.go new file mode 100644 index 00000000..d600850f --- /dev/null +++ b/operator/internal/rolecatalogue/catalogue.go @@ -0,0 +1,145 @@ +// Copyright 2026 ACC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +// Package rolecatalogue exposes the operator's compile-time list of valid +// agent roles. The catalogue is used by the AgentCollective validating +// webhook to give a clear "did you mean…" error when a CR uses a role name +// that is syntactically valid (passes the AgentRole regex) but doesn't +// correspond to any persona shipped under roles/. +// +// The catalogue is sourced from known_roles.txt, generated from the live +// roles/ directory by: +// +// go run ./hack/gen-catalogue.go +// +// (also reachable via `go generate ./internal/rolecatalogue/...`). Adding a +// new persona under roles//role.yaml + re-running the generator is +// all that's needed to widen the catalogue — no Go-side edits. +package rolecatalogue + +import ( + _ "embed" + "sort" + "strings" +) + +//go:generate go run ../../hack/gen-catalogue.go + +//go:embed known_roles.txt +var knownRolesData string + +var knownRoles = parse(knownRolesData) + +// parse turns the embedded text file into a set. Lines starting with '#' and +// blank lines are ignored; whitespace is trimmed. +func parse(data string) map[string]struct{} { + out := make(map[string]struct{}) + for _, line := range strings.Split(data, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + out[line] = struct{}{} + } + return out +} + +// IsKnown reports whether role appears in the compile-time catalogue. +func IsKnown(role string) bool { + _, ok := knownRoles[role] + return ok +} + +// All returns the catalogue as a sorted slice. The result is freshly +// allocated so callers may mutate it. +func All() []string { + out := make([]string, 0, len(knownRoles)) + for r := range knownRoles { + out = append(out, r) + } + sort.Strings(out) + return out +} + +// Suggest returns up to n catalogue entries closest to role by Levenshtein +// distance, in increasing distance order. Ties broken alphabetically. The +// result excludes any entry with distance > 8 to keep the suggestions +// useful — beyond that the user almost certainly typed something +// unrelated. +func Suggest(role string, n int) []string { + if n <= 0 { + return nil + } + type scored struct { + name string + dist int + } + candidates := make([]scored, 0, len(knownRoles)) + for r := range knownRoles { + d := levenshtein(role, r) + if d > 8 { + continue + } + candidates = append(candidates, scored{name: r, dist: d}) + } + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].dist != candidates[j].dist { + return candidates[i].dist < candidates[j].dist + } + return candidates[i].name < candidates[j].name + }) + if len(candidates) > n { + candidates = candidates[:n] + } + out := make([]string, 0, len(candidates)) + for _, c := range candidates { + out = append(out, c.name) + } + return out +} + +// levenshtein computes edit distance between a and b. Two-row dynamic +// programming — O(len(a)*len(b)) time, O(min) space. +func levenshtein(a, b string) int { + ar := []rune(a) + br := []rune(b) + if len(ar) < len(br) { + ar, br = br, ar + } + if len(br) == 0 { + return len(ar) + } + prev := make([]int, len(br)+1) + curr := make([]int, len(br)+1) + for j := range prev { + prev[j] = j + } + for i := 1; i <= len(ar); i++ { + curr[0] = i + for j := 1; j <= len(br); j++ { + cost := 1 + if ar[i-1] == br[j-1] { + cost = 0 + } + curr[j] = min3(prev[j]+1, curr[j-1]+1, prev[j-1]+cost) + } + prev, curr = curr, prev + } + return prev[len(br)] +} + +func min3(a, b, c int) int { + m := a + if b < m { + m = b + } + if c < m { + m = c + } + return m +} diff --git a/operator/internal/rolecatalogue/known_roles.txt b/operator/internal/rolecatalogue/known_roles.txt new file mode 100644 index 00000000..4c7d0349 --- /dev/null +++ b/operator/internal/rolecatalogue/known_roles.txt @@ -0,0 +1,54 @@ +# Generated from roles/ — do not edit by hand. +# Regenerate via: go run ./hack/gen-catalogue.go +# (or: go generate ./internal/rolecatalogue/...) +# +# Source of truth: roles//role.yaml on the main branch. +# One role name per line, alphabetical. + +account_executive +analyst +arbiter +business_analyst +coding_agent +coding_agent_architect +coding_agent_dependency +coding_agent_implementer +coding_agent_reviewer +coding_agent_tester +compliance_officer +content_marketer +contract_analyst +customer_success_manager +customer_support_agent +data_engineer +demand_generation_specialist +devops_engineer +financial_analyst +fpa_analyst +hr_business_partner +ingester +it_operations_specialist +it_support_specialist +learning_development_specialist +marketing_analyst +ml_engineer +observer +operations_analyst +procurement_specialist +product_manager +product_marketer +project_manager +recruiter +research_competitor +research_critic +research_economist +research_planner +research_strategist +research_synthesizer +revenue_operations_analyst +risk_compliance_analyst +sales_development_rep +sales_engineer +security_analyst +synthesizer +technical_support_specialist diff --git a/operator/test/unit/role_catalogue_test.go b/operator/test/unit/role_catalogue_test.go new file mode 100644 index 00000000..eb7fabde --- /dev/null +++ b/operator/test/unit/role_catalogue_test.go @@ -0,0 +1,179 @@ +// Copyright 2026 ACC Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 + +package unit_test + +import ( + "sort" + "testing" + + "github.com/redhat-ai-dev/agentic-cell-corpus/operator/internal/rolecatalogue" +) + +// TestIsKnown_LegacyRoles confirms the original ACCv3 5-role set is still +// in the catalogue. Removing any of these silently would break every +// existing AgentCollective custom resource at admission time. +func TestIsKnown_LegacyRoles(t *testing.T) { + for _, role := range []string{ + "ingester", "analyst", "synthesizer", "arbiter", "observer", + } { + if !rolecatalogue.IsKnown(role) { + t.Errorf("legacy role %q dropped from catalogue", role) + } + } +} + +// TestIsKnown_CodingSplitPersonas confirms the D3 split-skills personas +// are admitted. Used by examples/coding_split_skills/. +func TestIsKnown_CodingSplitPersonas(t *testing.T) { + for _, role := range []string{ + "coding_agent", + "coding_agent_architect", + "coding_agent_dependency", + "coding_agent_implementer", + "coding_agent_reviewer", + "coding_agent_tester", + } { + if !rolecatalogue.IsKnown(role) { + t.Errorf("coding-split persona %q missing from catalogue", role) + } + } +} + +// TestIsKnown_ResearchPersonas confirms the E4 autoresearcher personas +// are admitted. Used by examples/acc_autoresearcher/. +func TestIsKnown_ResearchPersonas(t *testing.T) { + for _, role := range []string{ + "research_planner", + "research_strategist", + "research_economist", + "research_competitor", + "research_synthesizer", + "research_critic", + } { + if !rolecatalogue.IsKnown(role) { + t.Errorf("research persona %q missing from catalogue", role) + } + } +} + +// TestIsKnown_RejectsUnknown spot-checks a handful of plausible-but-fake +// role names so the catalogue isn't accidentally wide-open. +func TestIsKnown_RejectsUnknown(t *testing.T) { + for _, role := range []string{ + "", + "fnord", + "_base", // skipped by the generator on purpose + "TEMPLATE", // skipped by the generator on purpose + "INGESTER", // case-sensitive — uppercase variant must not match + "researcher", // close to research_* but not present + } { + if rolecatalogue.IsKnown(role) { + t.Errorf("catalogue should reject %q", role) + } + } +} + +// TestAll_SortedAndUnique validates the public help-text accessor. +func TestAll_SortedAndUnique(t *testing.T) { + all := rolecatalogue.All() + if len(all) == 0 { + t.Fatal("All() returned empty slice — catalogue is empty?") + } + if !sort.StringsAreSorted(all) { + t.Errorf("All() must return sorted slice, got %v", all) + } + seen := map[string]struct{}{} + for _, r := range all { + if _, dup := seen[r]; dup { + t.Errorf("All() returned duplicate: %q", r) + } + seen[r] = struct{}{} + } +} + +// TestAll_AllowsCallerMutation confirms All() returns a fresh allocation +// — the public API contract states callers may mutate the result. +func TestAll_AllowsCallerMutation(t *testing.T) { + a := rolecatalogue.All() + if len(a) == 0 { + t.Skip("catalogue is empty") + } + a[0] = "MUTATED" + b := rolecatalogue.All() + if b[0] == "MUTATED" { + t.Errorf("All() must return a fresh slice; mutation leaked") + } +} + +// TestSuggest_TypoVariants exercises the closest-match heuristic on +// realistic typos. The exact ranking can shift if the catalogue +// changes; we only assert that the *intended* role appears in the top +// suggestions. +func TestSuggest_TypoVariants(t *testing.T) { + cases := []struct { + input string + mustHave string + }{ + {"research_plan", "research_planner"}, + {"research_economis", "research_economist"}, + {"coding_agnt_architect", "coding_agent_architect"}, + {"coding_agent_implmenter", "coding_agent_implementer"}, + {"analyzt", "analyst"}, + {"observator", "observer"}, + } + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + got := rolecatalogue.Suggest(c.input, 3) + if !contains(got, c.mustHave) { + t.Errorf("Suggest(%q, 3) = %v; expected to contain %q", + c.input, got, c.mustHave) + } + }) + } +} + +// TestSuggest_NCap enforces the n cap. +func TestSuggest_NCap(t *testing.T) { + got := rolecatalogue.Suggest("xxx", 2) + if len(got) > 2 { + t.Errorf("Suggest exceeded n=2 cap: got %d entries", len(got)) + } +} + +// TestSuggest_ZeroN documents the n<=0 contract. +func TestSuggest_ZeroN(t *testing.T) { + for _, n := range []int{0, -1, -100} { + if got := rolecatalogue.Suggest("research_plan", n); got != nil { + t.Errorf("Suggest(_, %d) = %v; want nil", n, got) + } + } +} + +// TestSuggest_DistanceCutoff confirms genuinely unrelated input gets no +// suggestions instead of nonsense matches. The cutoff is an +// implementation choice (currently distance ≤ 8) that callers can +// observe through this contract. +func TestSuggest_DistanceCutoff(t *testing.T) { + // 30 chars of 'q' is far enough from every real role that no + // suggestion should fit under the distance cap. + farInput := "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + if got := rolecatalogue.Suggest(farInput, 5); len(got) > 0 { + t.Errorf("Suggest(%q, 5) returned %v; expected no matches above distance cutoff", + farInput, got) + } +} + +func contains(haystack []string, needle string) bool { + for _, h := range haystack { + if h == needle { + return true + } + } + return false +}