From 2946df06f7eea79f5081c08ab24855f697cb54a2 Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Tue, 12 May 2026 15:52:00 -0400 Subject: [PATCH 1/2] Add agentic deploy scripts, CRDs, and operator integration Adds the agentic stack deployment infrastructure to lightspeed-operator: Deploy scripts (hack/agentic/): - deploy.sh: Full deploy with on-cluster builds (--provider=vertex|bedrock) - redeploy-{operator,agent,console,skills,all}.sh: Fast iteration scripts - undeploy.sh: Teardown with timeout + finalizer cleanup for stuck CRDs - lib.sh: Shared build helpers with _run() error wrapper, unified _build sync|async, centralized image vars, --wait on oc start-build Operator integration: - cmd/main.go: Wire agentic controller with --agentic-console-image and --agentic-sandbox-image flags - Add LightspeedAgents to FeatureGate enum in OLSConfig CRD - Agentic CRDs: ApprovalPolicy, Agent, LLMProvider, Proposal, results - config/rbac-agentic/: RBAC for agentic controller - Dockerfile.dev: Local module builds for agentic-operator dependency - Add FeatureGateLightspeedAgents and agentic image default constants Demo proposals use find-token skill from quay.io/harpatil/agentic-skills (TODO: replace with Konflux-built image when available). Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile.dev | 40 + Makefile | 3 +- api/v1alpha1/olsconfig_types.go | 4 +- cmd/main.go | 32 + .../bases/agentic.openshift.io_agents.yaml | 226 ++ .../agentic.openshift.io_analysisresults.yaml | 625 +++++ ...agentic.openshift.io_approvalpolicies.yaml | 122 + .../agentic.openshift.io_componenttools.yaml | 448 ++++ ...gentic.openshift.io_escalationresults.yaml | 183 ++ ...agentic.openshift.io_executionresults.yaml | 264 ++ .../agentic.openshift.io_llmproviders.yaml | 423 +++ ...gentic.openshift.io_proposalapprovals.yaml | 302 +++ .../bases/agentic.openshift.io_proposals.yaml | 2278 +++++++++++++++++ ...gentic.openshift.io_proposaltemplates.yaml | 124 + ...ntic.openshift.io_verificationresults.yaml | 235 ++ .../bases/agentic.openshift.io_workflows.yaml | 192 ++ .../bases/ols.openshift.io_olsconfigs.yaml | 3 +- config/crd/kustomization.yaml | 12 + config/default/deployment-patch.yaml | 6 + config/rbac-agentic/admin_role.yaml | 87 + config/rbac-agentic/component_owner_role.yaml | 80 + config/rbac-agentic/kustomization.yaml | 5 + config/rbac-agentic/role.yaml | 67 + config/rbac-agentic/role_binding.yaml | 17 + go.mod | 25 +- go.sum | 50 +- hack/agentic/CLAUDE.md | 74 + hack/agentic/deploy.sh | 263 ++ hack/agentic/lib.sh | 951 +++++++ hack/agentic/redeploy-agent.sh | 61 + hack/agentic/redeploy-all.sh | 61 + hack/agentic/redeploy-console.sh | 22 + hack/agentic/redeploy-operator.sh | 26 + hack/agentic/redeploy-skills.sh | 23 + hack/agentic/undeploy.sh | 64 + hack/image_placeholders.json | 10 + internal/agentic/setup.go | 61 + internal/controller/utils/constants.go | 7 + related_images.json | 12 +- 39 files changed, 7457 insertions(+), 31 deletions(-) create mode 100644 Dockerfile.dev create mode 100644 config/crd/bases/agentic.openshift.io_agents.yaml create mode 100644 config/crd/bases/agentic.openshift.io_analysisresults.yaml create mode 100644 config/crd/bases/agentic.openshift.io_approvalpolicies.yaml create mode 100644 config/crd/bases/agentic.openshift.io_componenttools.yaml create mode 100644 config/crd/bases/agentic.openshift.io_escalationresults.yaml create mode 100644 config/crd/bases/agentic.openshift.io_executionresults.yaml create mode 100644 config/crd/bases/agentic.openshift.io_llmproviders.yaml create mode 100644 config/crd/bases/agentic.openshift.io_proposalapprovals.yaml create mode 100644 config/crd/bases/agentic.openshift.io_proposals.yaml create mode 100644 config/crd/bases/agentic.openshift.io_proposaltemplates.yaml create mode 100644 config/crd/bases/agentic.openshift.io_verificationresults.yaml create mode 100644 config/crd/bases/agentic.openshift.io_workflows.yaml create mode 100644 config/rbac-agentic/admin_role.yaml create mode 100644 config/rbac-agentic/component_owner_role.yaml create mode 100644 config/rbac-agentic/kustomization.yaml create mode 100644 config/rbac-agentic/role.yaml create mode 100644 config/rbac-agentic/role_binding.yaml create mode 100644 hack/agentic/CLAUDE.md create mode 100755 hack/agentic/deploy.sh create mode 100755 hack/agentic/lib.sh create mode 100755 hack/agentic/redeploy-agent.sh create mode 100755 hack/agentic/redeploy-all.sh create mode 100755 hack/agentic/redeploy-console.sh create mode 100755 hack/agentic/redeploy-operator.sh create mode 100755 hack/agentic/redeploy-skills.sh create mode 100755 hack/agentic/undeploy.sh create mode 100644 internal/agentic/setup.go diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..71236273c --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,40 @@ +# Dev build — uses local lightspeed-agentic-operator source instead of fetching from GitHub. +# Build context must be the workspace root (parent of lightspeed-operator/). +# +# Usage: docker build -f lightspeed-operator/Dockerfile.dev -t operator:dev . +FROM registry.redhat.io/ubi9/go-toolset:9.7-1777043046 AS builder + +WORKDIR /workspace + +# Copy the local agentic operator module first +COPY lightspeed-agentic-operator/ /workspace/lightspeed-agentic-operator/ + +# Copy the operator module manifests +COPY lightspeed-operator/go.mod lightspeed-operator/go.sum ./ + +USER 0 + +# Point to the local agentic operator instead of the remote +RUN go mod edit -replace github.com/openshift/lightspeed-agentic-operator=./lightspeed-agentic-operator \ + && go mod edit -replace github.com/openshift/lightspeed-agentic-operator/api=./lightspeed-agentic-operator/api \ + && go mod edit -replace github.com/harche/lightspeed-agentic-operator=./lightspeed-agentic-operator + +RUN go mod download + +# Copy the operator source +COPY lightspeed-operator/cmd/ cmd/ +COPY lightspeed-operator/api/ api/ +COPY lightspeed-operator/internal/ internal/ +COPY lightspeed-operator/LICENSE /licenses/ + +USER 0 + +RUN CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} go build -a -tags strictfipsruntime -o manager cmd/main.go + +FROM registry.redhat.io/ubi9/ubi-minimal:9.7-1776645941 +WORKDIR / +COPY --from=builder /workspace/manager . +RUN mkdir /licenses +COPY lightspeed-operator/LICENSE /licenses/. +USER 65532:65532 +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index 5981f013a..af0248f35 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,8 @@ OPERATOR_SDK_VERSION ?= v1.36.1 IMG ?= $(IMAGE_TAG_BASE):$(VERSION) export IMG # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION = 1.27.1 +# lightspeed Agentic Operator uses >= 1.31 for CRD CEL rules (format.dns1123Subdomain). +ENVTEST_K8S_VERSION = 1.35.0 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) diff --git a/api/v1alpha1/olsconfig_types.go b/api/v1alpha1/olsconfig_types.go index 9fcdddab3..fdd6c2ee5 100644 --- a/api/v1alpha1/olsconfig_types.go +++ b/api/v1alpha1/olsconfig_types.go @@ -40,13 +40,13 @@ type OLSConfigSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="MCP Server Settings" MCPServers []MCPServerConfig `json:"mcpServers,omitempty"` // Feature Gates holds list of features to be enabled explicitly, otherwise they are disabled by default. - // possible values: MCPServer, ToolFiltering + // possible values: MCPServer, ToolFiltering, LightspeedAgents // +kubebuilder:validation:Optional // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Feature Gates" FeatureGates []FeatureGate `json:"featureGates,omitempty"` } -// +kubebuilder:validation:Enum=MCPServer;ToolFiltering +// +kubebuilder:validation:Enum=MCPServer;ToolFiltering;LightspeedAgents type FeatureGate string // OLSConfigStatus defines the observed state of OLS deployment. diff --git a/cmd/main.go b/cmd/main.go index 4242daee9..b9e6e23f1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -81,8 +81,11 @@ import ( olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" + agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/openshift/lightspeed-operator/internal/agentic" "github.com/openshift/lightspeed-operator/internal/controller" "github.com/openshift/lightspeed-operator/internal/controller/utils" utiltls "github.com/openshift/lightspeed-operator/internal/tls" @@ -114,6 +117,7 @@ func init() { utilruntime.Must(configv1.AddToScheme(scheme)) utilruntime.Must(olsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(agenticv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -179,6 +183,10 @@ func main() { var openshiftMCPServerImage string var dataverseExporterImage string var ocpRagImage string + var agenticConsoleImage string + var agenticSandboxImage string + var useLCore bool + var lcoreServerMode bool flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -198,6 +206,10 @@ func main() { flag.StringVar(&openshiftMCPServerImage, "openshift-mcp-server-image", utils.OpenShiftMCPServerImageDefault, "The image of the OpenShift MCP server container.") flag.StringVar(&dataverseExporterImage, "dataverse-exporter-image", utils.DataverseExporterImageDefault, "The image of the dataverse exporter container.") flag.StringVar(&ocpRagImage, "ocp-rag-image", utils.OcpRagImageDefault, "The image with the OCP RAG databases.") + flag.StringVar(&agenticConsoleImage, "agentic-console-image", utils.AgenticConsoleImageDefault, "The image of the agentic console plugin container.") + flag.StringVar(&agenticSandboxImage, "agentic-sandbox-image", utils.AgenticSandboxImageDefault, "The image of the agentic sandbox container.") + flag.BoolVar(&useLCore, "use-lcore", false, "Use LCore instead of AppServer for the application server deployment.") + flag.BoolVar(&lcoreServerMode, "lcore-server", true, "Use LCore in a server mode.") opts := zap.Options{ Development: true, } @@ -432,6 +444,26 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "OLSConfig") os.Exit(1) } + var agenticEnabled bool + { + olscfg := &olsv1alpha1.OLSConfig{} + if err := k8sClient.Get(ctx, types.NamespacedName{Name: utils.OLSConfigName}, olscfg); err == nil { + agenticEnabled = slices.Contains(olscfg.Spec.FeatureGates, olsv1alpha1.FeatureGate(utils.FeatureGateLightspeedAgents)) + } + } + if agenticEnabled { + if err = agentic.Setup(mgr, agentic.Options{ + Namespace: namespace, + ConsoleImage: agenticConsoleImage, + SandboxImage: agenticSandboxImage, + }); err != nil { + setupLog.Error(err, "unable to set up agentic controllers") + os.Exit(1) + } + setupLog.Info("Agentic controllers registered") + } else { + setupLog.Info("LightspeedAgents feature gate not enabled — skipping agentic controllers") + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/agentic.openshift.io_agents.yaml b/config/crd/bases/agentic.openshift.io_agents.yaml new file mode 100644 index 000000000..c713c4da7 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_agents.yaml @@ -0,0 +1,226 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: agents.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: Agent + listKind: AgentList + plural: agents + singular: agent + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.llmProvider.name + name: LLM + type: string + - jsonPath: .spec.model + name: Model + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "Agent defines a cluster-scoped agent tier (e.g., \"default\", + \"smart\", \"fast\").\nThe cluster admin creates Agent resources to configure + LLM infrastructure\nand runtime settings. Proposals reference agents by + name per step.\n\nAgent is cluster-scoped. The metadata.name serves as the + tier identifier.\nThe \"default\" agent must exist; \"smart\" and \"fast\" + are optional (the\noperator auto-links to \"default\" if absent).\n\nExample + — a high-capability agent tier:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Agent\n\tmetadata:\n\t name: smart\n\tspec:\n\t llmProvider:\n\t name: + vertex-ai\n\t model: claude-opus-4-6\n\t timeouts:\n\t analysisSeconds: + 300\n\t executionSeconds: 600\n\t maxTurns: 200\n\nExample — a fast, + cost-efficient agent tier:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Agent\n\tmetadata:\n\t name: fast\n\tspec:\n\t llmProvider:\n\t name: + vertex-ai\n\t model: claude-haiku-4-5\n\t timeouts:\n\t analysisSeconds: + 120\n\t executionSeconds: 300\n\t maxTurns: 100" + 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: spec defines the desired state of Agent. + properties: + llmProvider: + description: |- + llmProvider references a cluster-scoped LLMProvider CR that supplies the + LLM backend for this agent tier. + properties: + name: + description: name of the LLMProvider. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + maxTurns: + description: |- + maxTurns is the maximum number of tool-use turns the agent may take + in a single step invocation. Prevents runaway loops. + When omitted, the agent sandbox uses its built-in default. + Minimum 1, maximum 500. + format: int32 + maximum: 500 + minimum: 1 + type: integer + model: + description: |- + model is the LLM model identifier as recognized by the provider + (e.g., "claude-opus-4-6", "claude-haiku-4-5", "gpt-4o"). + Must start with an alphanumeric character and may contain + alphanumerics, dots, hyphens, underscores, slashes, colons, + and at-signs. Maximum 256 characters. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: model must start with an alphanumeric character and contain + only alphanumerics, dots, hyphens, underscores, slashes, colons, + and at-signs + rule: self.matches('^[a-zA-Z0-9][a-zA-Z0-9._\\-/:@]*$') + timeouts: + description: |- + timeouts configures per-step and per-turn timeout limits. + When omitted, the agent sandbox uses its built-in defaults. + minProperties: 1 + properties: + analysisSeconds: + description: analysisSeconds is the timeout for the analysis step + in seconds. + format: int32 + maximum: 3600 + minimum: 1 + type: integer + chatSeconds: + description: chatSeconds is the timeout for each chat turn with + the LLM in seconds. + format: int32 + maximum: 600 + minimum: 1 + type: integer + executionSeconds: + description: executionSeconds is the timeout for the execution + step in seconds. + format: int32 + maximum: 3600 + minimum: 1 + type: integer + verificationSeconds: + description: verificationSeconds is the timeout for the verification + step in seconds. + format: int32 + maximum: 3600 + minimum: 1 + type: integer + type: object + required: + - llmProvider + - model + type: object + status: + description: status defines the observed state of Agent. + minProperties: 1 + properties: + conditions: + description: |- + conditions represent the latest available observations of the + Agent's state. The Ready condition summarizes whether all + referenced resources (LLMProvider, Secrets) are present. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_analysisresults.yaml b/config/crd/bases/agentic.openshift.io_analysisresults.yaml new file mode 100644 index 000000000..62922c528 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_analysisresults.yaml @@ -0,0 +1,625 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: analysisresults.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: AnalysisResult + listKind: AnalysisResultList + plural: analysisresults + singular: analysisresult + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.proposalName + name: Proposal + type: string + - jsonPath: .status.conditions[?(@.type=="Completed")].reason + name: Outcome + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + AnalysisResult records the output of a single analysis step execution. + Created by the operator after the analysis agent completes. Owned by + the parent Proposal for garbage collection. + 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: spec contains the immutable identity fields for this result. + properties: + proposalName: + description: proposalName is the name of the parent Proposal in the + same namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - proposalName + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + description: status contains result data and conditions. + minProperties: 1 + properties: + conditions: + description: conditions track the lifecycle of this result. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + failureReason: + description: failureReason is populated when the step failed due to + a system error. + maxLength: 8192 + minLength: 1 + type: string + options: + description: options contains the remediation options returned by + the analysis agent. + items: + description: |- + RemediationOption represents a single remediation approach produced by + the analysis agent. The agent may return multiple options, each with + its own diagnosis, remediation plan, verification strategy, and RBAC + requirements. When the user approves execution, the operator trims + the AnalysisResult to keep only the approved option and uses its + RBAC and plan for the execution step. + + The components field is an extensibility point for adapter-specific UI + data. For example, an ACS adapter might include violation details or + affected deployment information as components that the console plugin + renders with custom components. + properties: + components: + description: |- + components contains optional adapter-defined structured data whose + shape is determined by spec.analysisOutput.schema on the Proposal. + The operator passes this through to the AnalysisResult CR; the + console renders it using adapter-specific UI components. + x-kubernetes-preserve-unknown-fields: true + diagnosis: + description: |- + diagnosis contains the root cause analysis specific to this option. + Present when analysisOutput mode is Default (or omitted). Omitted + when mode is Minimal. + properties: + confidence: + description: |- + confidence is the agent's self-assessed confidence in its diagnosis. + Higher confidence generally correlates with clearer symptoms and + more deterministic root causes. + enum: + - Low + - Medium + - High + type: string + rootCause: + description: |- + rootCause is a concise Markdown-formatted description of the identified + root cause (e.g., "OOMKilled due to memory limit of 256Mi"). + Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + summary: + description: |- + summary is a Markdown-formatted diagnosis summary explaining the + problem, its symptoms, and the agent's findings. Maximum 8192 characters. + maxLength: 8192 + minLength: 1 + type: string + required: + - confidence + - rootCause + - summary + type: object + proposal: + description: |- + proposal contains the remediation plan for this option. + Present when analysisOutput mode is Default (or omitted). Omitted + when mode is Minimal without an execution step. + properties: + actions: + description: |- + actions is the ordered list of discrete actions the agent proposes. + Maximum 50 items. + items: + description: |- + ProposedAction describes a single discrete action the analysis agent + recommends as part of its remediation plan. Actions are displayed to + the user after analysis for review before approval. + properties: + description: + description: |- + description is a Markdown-formatted explanation of what this action + will do (e.g., "Increase memory limit from 256Mi to 512Mi"). + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + type: + description: |- + type is the action category (e.g., "patch", "scale", "restart", + "create", "delete", "rollout"). Free-form string to allow agents + to express domain-specific action types. Must be 1-256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - description + - type + type: object + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + description: + description: |- + description is a Markdown-formatted summary of the overall remediation + approach. Maximum 8192 characters. + maxLength: 8192 + minLength: 1 + type: string + estimatedImpact: + description: |- + estimatedImpact is a Markdown-formatted description of the expected + impact of the remediation on the system + (e.g., "Brief pod restart, ~30s downtime"). + Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + reversible: + description: |- + reversible indicates whether the remediation can be rolled back + if something goes wrong. See rollbackPlan for details. + Must be one of: Reversible, Irreversible, Partial. + enum: + - Reversible + - Irreversible + - Partial + type: string + risk: + description: |- + risk is the agent's assessment of how risky the remediation is. + Critical-risk proposals typically require explicit human review. + enum: + - Low + - Medium + - High + - Critical + type: string + rollbackPlan: + description: |- + rollbackPlan describes how to undo the remediation if execution fails + or causes unexpected issues. Only the execution step mutates cluster + state, so rollback lives here alongside the actions it would undo. + properties: + command: + description: |- + command is the rollback command or steps to execute. + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + description: + description: |- + description is a Markdown-formatted explanation of the rollback strategy. + Must be 1-4096 characters. + maxLength: 4096 + minLength: 1 + type: string + required: + - description + type: object + required: + - actions + - description + - estimatedImpact + - risk + type: object + rbac: + description: |- + rbac contains the RBAC permissions the execution agent will need. + The operator's policy engine validates these before creating the + actual Kubernetes RBAC resources. Omitted for advisory-only options. + minProperties: 1 + properties: + clusterScoped: + description: |- + clusterScoped are rules that will be applied via ClusterRole + + ClusterRoleBinding. Used when the agent needs cross-namespace or + non-namespaced resource access (e.g., reading nodes, CRDs). + Maximum 50 items. + items: + description: |- + RBACRule describes a single RBAC permission that the analysis agent + requests for the execution step. The operator's policy engine validates + these requests against a 6-layer defense model before creating the + actual Role/ClusterRole bindings. Each rule must include a justification + so that users and policy can audit why the permission is needed. + properties: + apiGroups: + description: |- + apiGroups are the API groups for this rule (e.g., "", "apps", "batch"). + The empty string "" represents the core API group (pods, services, etc.). + Maximum 20 items, each up to 253 characters. + items: + maxLength: 253 + type: string + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + justification: + description: |- + justification is a Markdown-formatted explanation of why this + permission is needed for the remediation + (e.g., "Need to patch deployment to increase memory limit"). + Required for audit and policy enforcement. Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + namespace: + description: |- + namespace is the target namespace for namespace-scoped rules. + Must match one of the proposal's targetNamespaces. Ignored for + cluster-scoped rules. Validation is deferred to the operator's + policy engine at runtime. Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + resourceNames: + description: |- + resourceNames restricts the rule to specific named resources. + When empty, the rule applies to all resources of the given type. + Maximum 50 items. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + resources are the resource types (e.g., "pods", "deployments"). + Maximum 20 items. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + verbs: + description: |- + verbs are the allowed operations (e.g., "get", "patch", "delete"). + Maximum 10 items. + items: + maxLength: 63 + minLength: 1 + type: string + maxItems: 10 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - apiGroups + - justification + - resources + - verbs + type: object + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + namespaceScoped: + description: |- + namespaceScoped are rules that will be applied via Role + RoleBinding + in the proposal's target namespaces. These are the most common rules. + Maximum 50 items. + items: + description: |- + RBACRule describes a single RBAC permission that the analysis agent + requests for the execution step. The operator's policy engine validates + these requests against a 6-layer defense model before creating the + actual Role/ClusterRole bindings. Each rule must include a justification + so that users and policy can audit why the permission is needed. + properties: + apiGroups: + description: |- + apiGroups are the API groups for this rule (e.g., "", "apps", "batch"). + The empty string "" represents the core API group (pods, services, etc.). + Maximum 20 items, each up to 253 characters. + items: + maxLength: 253 + type: string + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + justification: + description: |- + justification is a Markdown-formatted explanation of why this + permission is needed for the remediation + (e.g., "Need to patch deployment to increase memory limit"). + Required for audit and policy enforcement. Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + namespace: + description: |- + namespace is the target namespace for namespace-scoped rules. + Must match one of the proposal's targetNamespaces. Ignored for + cluster-scoped rules. Validation is deferred to the operator's + policy engine at runtime. Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + resourceNames: + description: |- + resourceNames restricts the rule to specific named resources. + When empty, the rule applies to all resources of the given type. + Maximum 50 items. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + resources: + description: |- + resources are the resource types (e.g., "pods", "deployments"). + Maximum 20 items. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + verbs: + description: |- + verbs are the allowed operations (e.g., "get", "patch", "delete"). + Maximum 10 items. + items: + maxLength: 63 + minLength: 1 + type: string + maxItems: 10 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - apiGroups + - justification + - resources + - verbs + type: object + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + type: object + summary: + description: |- + summary is an optional Markdown-formatted one-line summary for + collapsed views in the console UI. Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + title: + description: |- + title is a short Markdown-formatted name for this option + (e.g., "Increase memory limit", "Restart with backoff"). + Must be 1-256 characters. + maxLength: 256 + minLength: 1 + type: string + verification: + description: |- + verification contains the verification plan. Omitted when + verification is skipped in the workflow. + properties: + description: + description: |- + description is a Markdown-formatted summary of the verification approach. + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + steps: + description: |- + steps is the ordered list of verification checks to run. + Maximum 20 items. + items: + description: |- + VerificationStep describes a single verification check that the + verification agent should run after execution. Populated by the + analysis agent as part of the RemediationOption. + properties: + command: + description: |- + command is the command or API call to run for this check + (e.g., "oc get pod -n production -l app=web -o jsonpath='{.items[0].status.phase}'"). + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + expected: + description: |- + expected is the expected output or condition + (e.g., "Running", "ready=true"). Maximum 1024 characters. + maxLength: 1024 + minLength: 1 + type: string + name: + description: |- + name is a short identifier for this check (e.g., "pod-running"). + Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + type: + description: |- + type categorizes the check (e.g., "command", "metric", "condition"). + Must be 1-256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - name + - type + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - description + type: object + required: + - title + type: object + x-kubernetes-validations: + - message: proposal is required when diagnosis is present + rule: '!has(self.diagnosis) || has(self.proposal)' + - message: diagnosis is required when proposal is present + rule: '!has(self.proposal) || has(self.diagnosis)' + maxItems: 10 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + sandbox: + description: sandbox tracks the sandbox pod used for this analysis. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic character + and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_approvalpolicies.yaml b/config/crd/bases/agentic.openshift.io_approvalpolicies.yaml new file mode 100644 index 000000000..007345a7e --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_approvalpolicies.yaml @@ -0,0 +1,122 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: approvalpolicies.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: ApprovalPolicy + listKind: ApprovalPolicyList + plural: approvalpolicies + singular: approvalpolicy + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "ApprovalPolicy is a cluster-scoped singleton that configures + default\napproval behavior for proposal workflow steps. The cluster admin + creates\na single ApprovalPolicy named \"cluster\" to control which steps + auto-approve.\n\nSteps not listed in the policy default to Manual (require + explicit\nuser approval on the ProposalApproval resource).\n\nExample:\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: ApprovalPolicy\n\tmetadata:\n\t name: + cluster\n\tspec:\n\t stages:\n\t - name: Analysis\n\t approval: + Automatic\n\t - name: Execution\n\t approval: Manual\n\t - name: + Verification\n\t approval: Automatic" + 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: spec defines the desired approval policy. + minProperties: 1 + properties: + maxAttempts: + description: |- + maxAttempts sets the maximum number of execution retry attempts + allowed for proposals. When verification fails, the operator retries + execution up to this limit before escalating. Defaults to 1 if omitted. + format: int32 + maximum: 3 + minimum: 1 + type: integer + maxConcurrentProposals: + default: 5 + description: |- + maxConcurrentProposals sets the maximum number of proposals the + operator reconciles concurrently. Higher values allow more proposals + to run in parallel but consume more cluster resources. + Defaults to 5 if omitted. + format: int32 + maximum: 20 + minimum: 1 + type: integer + stages: + description: |- + stages configures the approval mode for each workflow step. + Omitted steps default to Manual. + items: + description: ApprovalPolicyStage configures the approval mode for + a single workflow step. + properties: + approval: + description: |- + approval controls whether this step auto-approves or requires + explicit user approval on the ProposalApproval resource. + Allowed values: Automatic (step runs without user approval), + Manual (step waits for explicit approval on ProposalApproval). + enum: + - Automatic + - Manual + type: string + name: + description: |- + name is the workflow step this policy applies to. + Allowed values: Analysis, Execution, Verification, Escalation. + enum: + - Analysis + - Execution + - Verification + - Escalation + type: string + required: + - approval + - name + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: ApprovalPolicy must be named 'cluster' (singleton) + rule: self.metadata.name == 'cluster' + served: true + storage: true + subresources: {} diff --git a/config/crd/bases/agentic.openshift.io_componenttools.yaml b/config/crd/bases/agentic.openshift.io_componenttools.yaml new file mode 100644 index 000000000..478420592 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_componenttools.yaml @@ -0,0 +1,448 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: componenttools.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: ComponentTools + listKind: ComponentToolsList + plural: componenttools + singular: componenttools + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.skills[0].image + name: Skills Image + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "ComponentTools defines the domain-specific tools and configuration + that a\ncomponent owner (CVO, ACS, CMO, etc.) brings to the agentic platform. + It\nis owned by the component team and lives in their namespace alongside + any\nrequired secrets.\n\nComponentTools is referenced by Workflow steps + via componentTools. The\noperator combines it with a cluster-scoped Agent + (selected by tier) to\nproduce the full sandbox configuration at runtime.\n\nExample + — ACS security tools:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + ComponentTools\n\tmetadata:\n\t name: acs-tools\n\t namespace: stackrox\n\tspec:\n\t + \ skills:\n\t - image: quay.io/stackrox/acs-skills:latest\n\t mcpServers:\n\t + \ - name: acs-api\n\t url: https://central.stackrox.svc:8443/v1\n\t + \ headers:\n\t - name: Authorization\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: acs-api-token\n\t + \ systemPrompt: |\n\t You are an ACS security remediation agent. Analyze + violations\n\t and propose fixes that address the security policy.\n\t + \ requiredSecrets:\n\t - name: acs-api-token\n\t description: \"ACS + Central API token for querying violations\"\n\t mountAs: ACS_API_TOKEN" + 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: spec defines the desired state of ComponentTools. + properties: + mcpServers: + description: |- + mcpServers defines external MCP (Model Context Protocol) servers the + agent can connect to for additional tools and context beyond its + built-in skills. Each server is identified by name and URL. + Maximum 20 items. + items: + description: "MCPServerConfig defines the configuration for an MCP + (Model Context Protocol)\nserver that the agent can connect to + for additional tools and context.\nMCP servers extend the agent's + capabilities beyond its built-in skills.\n\nExample — connecting + to an OpenShift MCP server with SA token auth:\n\n\tmcpServers:\n\t + \ - name: openshift\n\t url: https://mcp.openshift-lightspeed.svc:8443/sse\n\t + \ timeoutSeconds: 10\n\t headers:\n\t - name: Authorization\n\t + \ valueFrom:\n\t type: Kubernetes\n\nExample — + connecting to an external API with secret-based auth:\n\n\tmcpServers:\n\t + \ - name: pagerduty\n\t url: https://mcp-pagerduty.example.com/sse\n\t + \ headers:\n\t - name: X-API-Key\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: + pagerduty-api-key" + properties: + headers: + description: headers to send to the MCP server. Maximum 20 items. + items: + description: |- + MCPHeader defines an HTTP header to send with every request to an + MCP server. Used for authentication and routing. + properties: + name: + description: |- + name of the header (e.g., "Authorization", "X-API-Key"). + Must be at least 1 character, containing only letters, digits, and hyphens. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a letter and contain only + letters, digits, and hyphens + rule: self.matches('^[A-Za-z][A-Za-z0-9-]*$') + valueFrom: + description: valueFrom is the source of the header value. + properties: + secret: + description: |- + secret references a Secret containing the header value. + Required when type is "Secret". + properties: + name: + description: name of the Secret. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + type: + description: |- + type specifies the source type for the header value. Allowed values: + - "Secret" — reads the value from a Kubernetes Secret (use for + API keys and tokens). Requires the secret field to be set. + - "Kubernetes" — auto-injects a Kubernetes service account token + (for MCP servers that accept K8s auth). + - "Client" — the value is provided by the calling client at + runtime (e.g., forwarded from a user session). + enum: + - Secret + - Kubernetes + - Client + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: secret is required when type is 'Secret' + rule: 'self.type == ''Secret'' ? has(self.secret) : + true' + - message: secret must not be set when type is 'Kubernetes' + or 'Client' + rule: 'self.type != ''Secret'' ? !has(self.secret) : + true' + required: + - name + - valueFrom + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + name: + description: name of the MCP server. Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + timeoutSeconds: + default: 5 + description: |- + timeoutSeconds is the timeout for the MCP server in seconds, default is 5. + Valid range: 1-300. + format: int32 + maximum: 300 + minimum: 1 + type: integer + url: + description: |- + url of the MCP server (HTTP/HTTPS). Must be an HTTP or HTTPS URL, + maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: url must be a valid HTTP or HTTPS URL + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + required: + - name + - url + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + outputSchema: + description: |- + outputSchema is a JSON Schema object that defines additional structured + output fields beyond the base schema that every agent produces (diagnosis, + proposal, RBAC, verification plan). The operator merges this schema into + the base output schema sent to the agent. Use this to request + domain-specific structured data from the agent (e.g., an ACS adapter + might add a "violationId" string field). + x-kubernetes-preserve-unknown-fields: true + requiredSecrets: + description: |- + requiredSecrets declares Kubernetes Secrets that the sandbox pod needs + at runtime. The component owner defines what secrets are needed; the + cluster admin or customer creates the actual Secrets in the same + namespace as this ComponentTools resource. + Maximum 20 items. + items: + description: |- + SecretRequirement declares a Kubernetes Secret that the sandbox needs + at runtime. The component owner defines what secrets are needed; the + cluster admin or customer creates the actual Secret in the same namespace. + properties: + description: + description: |- + description explains what this secret is used for, helping the + cluster admin understand what credentials to provide. + maxLength: 1024 + minLength: 1 + type: string + mountAs: + description: |- + mountAs specifies how the secret is exposed in the sandbox pod. + Use an environment variable name (e.g., "GITHUB_TOKEN") to inject + as an env var, or a file path (e.g., "/etc/secrets/token") to + mount as a file. + maxLength: 512 + minLength: 1 + type: string + name: + description: name of the Secret (must exist in the same namespace + as the ComponentTools). + maxLength: 253 + minLength: 1 + type: string + required: + - mountAs + - name + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + skills: + description: |- + skills defines one or more OCI images containing skills to mount + in the agent's sandbox pod. Each entry specifies an image and optionally + which paths within that image to mount. The operator creates Kubernetes + image volumes (requires K8s 1.34+) and mounts them into the agent's + skills directory. Requires 1-20 items. + + Multiple entries allow composing skills from different images: + + skills: + - image: registry.ci.openshift.org/ocp/5.0:agentic-skills + paths: + - /skills/prometheus + - /skills/cluster-update/update-advisor + - image: quay.io/my-org/custom-skills:latest + items: + description: "SkillsSource defines an OCI image containing skills + and optionally which\npaths within that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's sandbox + pod.\n\nWhen paths is omitted, the entire image is mounted. When + paths is specified,\nonly those directories are mounted (each + as a separate subPath volumeMount),\nallowing selective composition + of skills from large shared images.\n\nExample — mount all skills + from a custom image:\n\n\tskills:\n\t - image: quay.io/my-org/my-skills:latest\n\nExample + — selectively mount two skills from a shared image:\n\n\tskills:\n\t + \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t + \ paths:\n\t - /skills/prometheus\n\t - /skills/cluster-update/update-advisor" + properties: + image: + description: |- + image is the OCI image reference containing skills. + The operator mounts this as a Kubernetes image volume (requires K8s 1.34+). + Must be a valid OCI image pullspec: a domain, followed by a repository path, + ending with either a tag (:tag) or a digest (@algorithm:hex). + Must be 1-512 characters. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the '.' character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + '.', '_', '__', '-' characters. + rule: self.find('(/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != '' + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != '' || self.find(':.*$') != '' + - message: tag must not be more than 127 characters + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character followed by word characters, '.', or '-' + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms must + start with an alpha character followed by alphanumeric characters + and may contain '-', '_', '+', and '.' characters. + rule: 'self.find(''(@.*:)'') != '''' ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest must be at least 32 characters + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest must only contain hex characters (A-F, a-f, + 0-9) + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + paths: + description: |- + paths restricts which directories from the image are mounted. + Each path is mounted as a separate subPath volumeMount into the agent's + skills directory. The last segment of each path becomes the mount name + (e.g., "/skills/prometheus" mounts as "prometheus"). + + Each path must be an absolute file path: starts with "/", no ".." + or "." segments, no double slashes, no trailing slash, and only + alphanumeric characters, hyphens, underscores, dots, and slashes. + + When omitted, the entire image is mounted as a single volume. + Maximum 50 items. + items: + maxLength: 512 + minLength: 2 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each path must be 2-512 characters + rule: self.all(p, p.size() >= 2 && p.size() <= 512) + - message: each path must be absolute (start with '/') + rule: self.all(p, p.startsWith('/')) + - message: paths must not end with '/' + rule: self.all(p, !p.endsWith('/')) + - message: paths must not contain double slashes + rule: self.all(p, !p.contains('//')) + - message: paths must not contain '.' or '..' segments + rule: self.all(p, !p.contains('/../') && !p.endsWith('/..') + && !p.contains('/./') && !p.endsWith('/.')) + - message: paths may only contain alphanumeric characters, '/', + '_', '.', and '-' + rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) + required: + - image + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + systemPrompt: + description: |- + systemPrompt is the system prompt text that shapes the agent's + behavior for its role (analysis, execution, or verification). + When omitted, the agent uses a default prompt appropriate for + its workflow step. Maximum 32768 characters. + maxLength: 32768 + minLength: 1 + type: string + required: + - skills + type: object + status: + description: status defines the observed state of ComponentTools. + properties: + conditions: + description: |- + conditions represent the latest available observations of the + ComponentTools' state. The Ready condition summarizes whether all + referenced resources (Secrets, OCI images) are present. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_escalationresults.yaml b/config/crd/bases/agentic.openshift.io_escalationresults.yaml new file mode 100644 index 000000000..bc54ccabe --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_escalationresults.yaml @@ -0,0 +1,183 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: escalationresults.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: EscalationResult + listKind: EscalationResultList + plural: escalationresults + singular: escalationresult + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.proposalName + name: Proposal + type: string + - jsonPath: .status.conditions[?(@.type=="Completed")].reason + name: Outcome + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + EscalationResult records the output of the escalation step. Created by + the operator after the escalation agent completes. Owned by the parent + Proposal for garbage collection. + 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: spec contains the immutable identity fields for this result. + properties: + proposalName: + description: proposalName is the name of the parent Proposal in the + same namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - proposalName + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + description: status contains result data and conditions. + minProperties: 1 + properties: + conditions: + description: conditions track the lifecycle of this result. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + content: + description: content is freeform escalation content produced by the + agent. + maxLength: 65536 + minLength: 1 + type: string + failureReason: + description: failureReason is populated when the step failed due to + a system error. + maxLength: 8192 + minLength: 1 + type: string + sandbox: + description: sandbox tracks the sandbox pod used for this escalation. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic character + and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + summary: + description: summary is a Markdown-formatted escalation summary. + maxLength: 32768 + minLength: 1 + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_executionresults.yaml b/config/crd/bases/agentic.openshift.io_executionresults.yaml new file mode 100644 index 000000000..e31a48831 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_executionresults.yaml @@ -0,0 +1,264 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: executionresults.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: ExecutionResult + listKind: ExecutionResultList + plural: executionresults + singular: executionresult + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.proposalName + name: Proposal + type: string + - jsonPath: .spec.retryIndex + name: Retry + type: integer + - jsonPath: .status.conditions[?(@.type=="Completed")].reason + name: Outcome + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + ExecutionResult records the output of a single execution step execution. + Created by the operator after the execution agent completes. Owned by + the parent Proposal for garbage collection. + 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: spec contains the immutable identity fields for this result. + properties: + proposalName: + description: proposalName is the name of the parent Proposal in the + same namespace. + maxLength: 253 + minLength: 1 + type: string + retryIndex: + description: |- + retryIndex is the 0-based retry index within the current analysis. + First execution has retryIndex 0, first retry has retryIndex 1, etc. + format: int32 + maximum: 2 + minimum: 0 + type: integer + required: + - proposalName + - retryIndex + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + description: status contains result data and conditions. + minProperties: 1 + properties: + actionsTaken: + description: actionsTaken lists what the agent did. + items: + description: |- + ExecutionAction describes a single action taken by the execution agent + during the execution step. These are recorded in ExecutionStepStatus + to provide an audit trail of what the agent actually did. + properties: + description: + description: |- + description is a Markdown-formatted explanation of what the agent did + (e.g., "Patched deployment/web to set memory limit to 512Mi"). + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + error: + description: |- + error is the error message if the action failed. + Maximum 8192 characters. + maxLength: 8192 + minLength: 1 + type: string + outcome: + description: |- + outcome indicates whether this individual action succeeded. + Must be one of: Succeeded, Failed. + enum: + - Succeeded + - Failed + type: string + output: + description: |- + output is the command output or API response from the action. + Maximum 32768 characters. + maxLength: 32768 + minLength: 1 + type: string + type: + description: |- + type is the action category (e.g., "patch", "scale", "restart"). + Maximum 256 characters. + maxLength: 256 + minLength: 1 + type: string + required: + - description + - outcome + - type + type: object + maxItems: 100 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + conditions: + description: conditions track the lifecycle of this result. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + failureReason: + description: failureReason is populated when the step failed due to + a system error. + maxLength: 8192 + minLength: 1 + type: string + sandbox: + description: sandbox tracks the sandbox pod used for this execution. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic character + and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + verification: + description: |- + verification is the lightweight inline verification the execution + agent performs immediately after completing its actions. + properties: + conditionOutcome: + description: |- + conditionOutcome indicates whether the target condition improved + after the remediation (e.g., pod is no longer CrashLoopBackOff). + Must be one of: Improved, Unchanged, Degraded. + enum: + - Improved + - Unchanged + - Degraded + type: string + summary: + description: |- + summary is a Markdown-formatted summary of the inline verification. + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + required: + - conditionOutcome + - summary + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_llmproviders.yaml b/config/crd/bases/agentic.openshift.io_llmproviders.yaml new file mode 100644 index 000000000..18509a788 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_llmproviders.yaml @@ -0,0 +1,423 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: llmproviders.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: LLMProvider + listKind: LLMProviderList + plural: llmproviders + singular: llmprovider + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.type + name: Type + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "LLMProvider defines an LLM provider configuration. It is the + first link in\nthe CRD chain (LLMProvider -> Agent -> Workflow -> Proposal) + and is\nreferenced by Agent resources via spec.llmProvider.\n\nLLMProvider + is cluster-scoped — the cluster admin manages LLM infrastructure\ncentrally. + The operator uses the credentials to configure the LLM client\ninside agent + sandbox pods. The model is specified on the Agent CR, allowing\nmultiple + agents to share one LLMProvider with different models.\n\nTypically you + create one provider per backend (e.g., one for Vertex AI)\nand then reference + it from multiple Agent resources with different models.\n\nExample — a Vertex + AI provider (model specified on Agent, not here):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + LLMProvider\n\tmetadata:\n\t name: vertex-ai\n\tspec:\n\t type: GoogleCloudVertex\n\t + \ googleCloudVertex:\n\t credentialsSecret:\n\t name: llm-credentials\n\t + \ projectID: my-gcp-project\n\t region: us-central1" + 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: spec defines the desired state of LLMProvider. + properties: + anthropic: + description: |- + anthropic contains Anthropic-specific configuration. + Required when type is "Anthropic". + properties: + credentialsSecret: + description: |- + credentialsSecret references a Secret in the operator namespace + (openshift-lightspeed) containing the Anthropic API credentials. + The Secret must contain the key ANTHROPIC_API_KEY. + properties: + name: + description: name of the Secret. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + url: + description: |- + url is an optional override for the Anthropic API endpoint. + Only needed for custom deployments or API proxies. + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + required: + - credentialsSecret + type: object + awsBedrock: + description: |- + awsBedrock contains AWS Bedrock-specific configuration. + Required when type is "AWSBedrock". + properties: + credentialsSecret: + description: |- + credentialsSecret references a Secret in the operator namespace + (openshift-lightspeed) containing AWS credentials. The Secret must + contain the keys AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. + properties: + name: + description: name of the Secret. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + region: + description: |- + region is the AWS region for the Bedrock endpoint. + Must begin with a lowercase letter and end with a lowercase + alphanumeric character. May contain lowercase letters, digits, + and hyphens (e.g., "us-east-1", "eu-west-2", "ap-southeast-1"). + maxLength: 63 + minLength: 2 + type: string + x-kubernetes-validations: + - message: region must contain only lowercase letters, digits, + and hyphens, start with a letter, and not end with a hyphen + rule: self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') + url: + description: |- + url is an optional override for the AWS Bedrock API endpoint. + Only needed for custom deployments or API proxies. + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + required: + - credentialsSecret + - region + type: object + azureOpenAI: + description: |- + azureOpenAI contains Azure OpenAI Service-specific configuration. + Required when type is "AzureOpenAI". + properties: + apiVersion: + description: |- + apiVersion is the Azure OpenAI API version. Azure API versions use + a date-based format: YYYY-MM-DD with an optional "-preview" suffix + (e.g., "2024-02-01", "2024-08-01-preview"). + When omitted, the SDK default is used. + maxLength: 32 + minLength: 1 + type: string + x-kubernetes-validations: + - message: apiVersion must be a date in YYYY-MM-DD format with + an optional -preview suffix + rule: self.matches('^[0-9]{4}-[0-9]{2}-[0-9]{2}(-preview)?$') + credentialsSecret: + description: |- + credentialsSecret references a Secret in the operator namespace + (openshift-lightspeed) containing the Azure OpenAI API credentials. + The Secret must contain the key AZURE_OPENAI_API_KEY. + properties: + name: + description: name of the Secret. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + endpoint: + description: |- + endpoint is the Azure OpenAI resource endpoint + (e.g., "https://my-resource.openai.azure.com"). + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + url: + description: |- + url is an optional override for the Azure OpenAI API endpoint. + Only needed for custom deployments or API proxies. This is separate + from the required 'endpoint' field which identifies the Azure resource. + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + required: + - credentialsSecret + - endpoint + type: object + googleCloudVertex: + description: |- + googleCloudVertex contains Google Cloud Vertex AI-specific configuration. + Required when type is "GoogleCloudVertex". + properties: + credentialsSecret: + description: |- + credentialsSecret references a Secret in the operator namespace + (openshift-lightspeed) containing a GCP service account JSON key. + The Secret must contain the key GOOGLE_APPLICATION_CREDENTIALS. + properties: + name: + description: name of the Secret. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + projectID: + description: |- + projectID is the Google Cloud Project ID where Vertex AI is enabled. + A Project ID is a globally unique identifier that must be 6 to 30 + characters in length, can only contain lowercase letters, digits, and + hyphens, must start with a letter, and cannot end with a hyphen. + maxLength: 30 + minLength: 6 + type: string + x-kubernetes-validations: + - message: projectID must start with a lowercase letter, contain + only lowercase letters, digits, and hyphens, and cannot end + with a hyphen + rule: self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') + region: + description: |- + region is the GCP region for the Vertex AI endpoint. + Must begin with a lowercase letter and end with a lowercase + alphanumeric character. May contain lowercase letters, digits, + and hyphens (e.g., "us-central1", "europe-west4", "asia-southeast1"). + maxLength: 63 + minLength: 2 + type: string + x-kubernetes-validations: + - message: region must contain only lowercase letters, digits, + and hyphens, start with a letter, and not end with a hyphen + rule: self.matches('^[a-z][a-z0-9-]*[a-z0-9]$') + url: + description: |- + url is an optional override for the Vertex AI API endpoint. + Only needed for custom deployments or API proxies. + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + required: + - credentialsSecret + - projectID + - region + type: object + openAI: + description: |- + openAI contains OpenAI-specific configuration. + Required when type is "OpenAI". + properties: + credentialsSecret: + description: |- + credentialsSecret references a Secret in the operator namespace + (openshift-lightspeed) containing the OpenAI API credentials. + The Secret must contain the key OPENAI_API_KEY. + properties: + name: + description: name of the Secret. Must be a valid RFC 1123 + DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + url: + description: |- + url is an optional override for the OpenAI API endpoint. + Only needed for custom deployments or API proxies. + Must be a valid HTTP or HTTPS URL with a hostname. Paths and query + parameters are allowed. Fragments and userinfo are not permitted. + Maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must use http or https scheme + rule: isURL(self) && url(self).getScheme() in ['http', 'https'] + - message: must include a hostname + rule: isURL(self) && url(self).getHostname() != '' + - message: userinfo is not allowed in URL + rule: '!self.contains(''@'')' + - message: fragments are not allowed in URL + rule: '!self.contains(''#'')' + required: + - credentialsSecret + type: object + type: + description: |- + type is a required field that configures which LLM provider backend + should be used. + + Allowed values are Anthropic, GoogleCloudVertex, OpenAI, AzureOpenAI, + and AWSBedrock. + + When set to Anthropic, agents referencing this provider will use the + Anthropic API directly, and the 'anthropic' field must be configured. + + When set to GoogleCloudVertex, agents referencing this provider will + use Google Cloud Vertex AI, and the 'googleCloudVertex' field must be + configured. + + When set to OpenAI, agents referencing this provider will use an + OpenAI-compatible API, and the 'openAI' field must be configured. + + When set to AzureOpenAI, agents referencing this provider will use + the Azure OpenAI Service, and the 'azureOpenAI' field must be + configured. + + When set to AWSBedrock, agents referencing this provider will use + AWS Bedrock, and the 'awsBedrock' field must be configured. + enum: + - Anthropic + - GoogleCloudVertex + - OpenAI + - AzureOpenAI + - AWSBedrock + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: anthropic is required when type is Anthropic, and forbidden + otherwise + rule: 'self.type == ''Anthropic'' ? has(self.anthropic) : !has(self.anthropic)' + - message: googleCloudVertex is required when type is GoogleCloudVertex, + and forbidden otherwise + rule: 'self.type == ''GoogleCloudVertex'' ? has(self.googleCloudVertex) + : !has(self.googleCloudVertex)' + - message: openAI is required when type is OpenAI, and forbidden otherwise + rule: 'self.type == ''OpenAI'' ? has(self.openAI) : !has(self.openAI)' + - message: azureOpenAI is required when type is AzureOpenAI, and forbidden + otherwise + rule: 'self.type == ''AzureOpenAI'' ? has(self.azureOpenAI) : !has(self.azureOpenAI)' + - message: awsBedrock is required when type is AWSBedrock, and forbidden + otherwise + rule: 'self.type == ''AWSBedrock'' ? has(self.awsBedrock) : !has(self.awsBedrock)' + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/config/crd/bases/agentic.openshift.io_proposalapprovals.yaml b/config/crd/bases/agentic.openshift.io_proposalapprovals.yaml new file mode 100644 index 000000000..41724f288 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_proposalapprovals.yaml @@ -0,0 +1,302 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: proposalapprovals.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: ProposalApproval + listKind: ProposalApprovalList + plural: proposalapprovals + singular: proposalapproval + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "ProposalApproval tracks per-step approval state for a Proposal. + The\noperator creates it when a Proposal is created. Users update it to\napprove + or deny individual workflow steps.\n\nProposalApproval has a 1:1 relationship + with its Proposal (same name,\nsame namespace) and is owned by the Proposal + via an owner reference\nfor garbage collection.\n\nExample:\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: ProposalApproval\n\tmetadata:\n\t + \ name: fix-crash\n\t namespace: my-namespace\n\t ownerReferences:\n\t + \ - apiVersion: agentic.openshift.io/v1alpha1\n\t kind: Proposal\n\t + \ name: fix-crash\n\tspec:\n\t stages:\n\t - type: Analysis\n\t + \ analysis: {}\n\t - type: Execution\n\t execution:\n\t option: + 0\n\t agent: fast" + 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: spec defines the desired approval state. + minProperties: 1 + properties: + stages: + description: |- + stages lists the approved (or denied) workflow steps. Each entry is + a discriminated union keyed by type. Users add stages one at a time + via patch as they approve each step. + items: + description: |- + ApprovalStage is a discriminated union representing approval for one + workflow step. Presence in spec.stages indicates approval; absence means + not yet approved (controller checks ApprovalPolicy for auto-approve). + properties: + analysis: + description: |- + analysis contains approval parameters for the analysis step. + Required when type is Analysis. + minProperties: 1 + properties: + agent: + default: default + description: agent is the Agent CR for this step. Defaults + to "default". + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + type: object + decision: + description: |- + decision indicates whether this stage is approved or denied. + Denying any stage terminates the entire proposal, even if + earlier stages were already approved. Once set to Denied, + it cannot be changed. + enum: + - Approved + - Denied + type: string + escalation: + description: |- + escalation contains approval parameters for the escalation step. + Required when type is Escalation. + minProperties: 1 + properties: + agent: + default: default + description: agent is the Agent CR for this step. Defaults + to "default". + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + type: object + execution: + description: |- + execution contains approval parameters for the execution step. + Required when type is Execution. + minProperties: 1 + properties: + agent: + default: default + description: agent is the Agent CR for this step. Defaults + to "default". + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + maxAttempts: + description: |- + maxAttempts is the number of execution retry attempts approved + for this proposal. Must not exceed ApprovalPolicy.spec.maxAttempts. + Defaults to 1 if unset. + format: int32 + maximum: 3 + minimum: 1 + type: integer + option: + description: |- + option is the 0-based index into the analysis options array + selecting which remediation approach to execute. + format: int32 + minimum: 0 + type: integer + type: object + type: + description: type identifies which workflow step this approval + is for. + enum: + - Analysis + - Execution + - Verification + - Escalation + type: string + verification: + description: |- + verification contains approval parameters for the verification step. + Required when type is Verification. + minProperties: 1 + properties: + agent: + default: default + description: agent is the Agent CR for this step. Defaults + to "default". + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: analysis is required when type is Analysis, and forbidden + otherwise + rule: 'self.type == ''Analysis'' ? has(self.analysis) : !has(self.analysis)' + - message: execution is required when type is Execution, and forbidden + otherwise + rule: 'self.type == ''Execution'' ? has(self.execution) : !has(self.execution)' + - message: verification is required when type is Verification, and + forbidden otherwise + rule: 'self.type == ''Verification'' ? has(self.verification) + : !has(self.verification)' + - message: escalation is required when type is Escalation, and forbidden + otherwise + rule: 'self.type == ''Escalation'' ? has(self.escalation) : !has(self.escalation)' + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + x-kubernetes-validations: + - message: 'stages are append-only: existing stages cannot be removed' + rule: oldSelf.stages.all(old, self.stages.exists(s, s.type == old.type)) + - message: decisions once set cannot be changed + rule: oldSelf.stages.all(old, !(has(old.decision) && old.decision == + 'Denied') || self.stages.exists(s, s.type == old.type && has(s.decision) + && s.decision == 'Denied')) + - message: maxAttempts once set cannot be changed + rule: oldSelf.stages.all(old, old.type != 'Execution' || !has(old.execution) + || !has(old.execution.maxAttempts) || old.execution.maxAttempts == + 0 || self.stages.exists(s, s.type == 'Execution' && has(s.execution) + && has(s.execution.maxAttempts) && s.execution.maxAttempts == old.execution.maxAttempts)) + status: + description: status defines the observed approval state. + minProperties: 1 + properties: + stages: + description: stages contains the per-stage approval status set by + the controller. + items: + description: ApprovalStageStatus is the observed state of a single + approval stage. + properties: + conditions: + description: conditions for this approval stage. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: name identifies the workflow step. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_proposals.yaml b/config/crd/bases/agentic.openshift.io_proposals.yaml new file mode 100644 index 000000000..c6dadf1eb --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_proposals.yaml @@ -0,0 +1,2278 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: proposals.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: Proposal + listKind: ProposalList + plural: proposals + singular: proposal + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.request + name: Request + priority: 1 + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "Proposal represents a unit of work managed by the agentic platform.\nIt + is the primary resource component teams and adapters interact with.\n\nA + Proposal defines the workflow shape inline: which steps run and which\nagent + handles each step. Analysis is always required. Omit execution\nand/or verification + to skip those steps.\n\nExample — analysis only (advisory):\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: Proposal\n\tmetadata:\n\t name: + one-off-investigation\n\tspec:\n\t request: \"Investigate why pod foo is + crashlooping\"\n\t targetNamespaces:\n\t - lightspeed-demo\n\t tools:\n\t + \ skills:\n\t - image: registry.redhat.io/acs/acs-agentic-skills:latest\n\t + \ analysis:\n\t agent: smart\n\nExample — full remediation (analyze → + execute → verify):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Proposal\n\tmetadata:\n\t name: fix-nginx-cve-2024-1234\n\t namespace: + stackrox\n\tspec:\n\t request: \"Fix CVE-2024-1234 in nginx:1.21\"\n\t + \ targetNamespaces:\n\t - lightspeed-demo\n\t tools:\n\t skills:\n\t + \ - image: registry.redhat.io/acs/acs-agentic-skills:latest\n\t requiredSecrets:\n\t + \ - name: acs-api-token\n\t mountAs:\n\t type: EnvVar\n\t + \ envVar:\n\t name: ACS_API_TOKEN\n\t analysis:\n\t + \ agent: smart\n\t execution: {}\n\t verification:\n\t agent: fast" + 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: spec defines the desired state of Proposal. + properties: + analysis: + description: |- + analysis defines per-step configuration for the analysis step, + including which agent handles it and any per-step tools. + + Immutable: agent and per-step tools are fixed at creation. + minProperties: 1 + properties: + agent: + description: |- + agent is the name of the cluster-scoped Agent CR to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + tools: + description: |- + tools provides per-step tools that replace the shared spec.tools + for this step. Use this when different steps need different skills. + minProperties: 1 + properties: + mcpServers: + description: |- + mcpServers defines external MCP (Model Context Protocol) servers the + agent can connect to for additional tools and context. + items: + description: "MCPServerConfig defines the configuration + for an MCP (Model Context Protocol)\nserver that the agent + can connect to for additional tools and context.\nMCP + servers extend the agent's capabilities beyond its built-in + skills.\n\nExample — connecting to an OpenShift MCP server + with SA token auth:\n\n\tmcpServers:\n\t - name: openshift\n\t + \ url: https://mcp.openshift-lightspeed.svc:8443/sse\n\t + \ timeoutSeconds: 10\n\t headers:\n\t - name: + Authorization\n\t valueFrom:\n\t type: + ServiceAccountToken\n\nExample — connecting to an external + API with secret-based auth:\n\n\tmcpServers:\n\t - name: + pagerduty\n\t url: https://mcp-pagerduty.example.com/sse\n\t + \ headers:\n\t - name: X-API-Key\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: + pagerduty-api-key" + properties: + headers: + description: headers to send to the MCP server. Maximum + 20 items. + items: + description: |- + MCPHeader defines an HTTP header to send with every request to an + MCP server. Used for authentication and routing. + properties: + name: + description: |- + name of the header (e.g., "Authorization", "X-API-Key"). + Must be at least 1 character, containing only letters, digits, and hyphens. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a letter and contain + only letters, digits, and hyphens + rule: self.matches('^[A-Za-z][A-Za-z0-9-]*$') + valueFrom: + description: valueFrom is the source of the header + value. + properties: + secret: + description: |- + secret references a Secret containing the header value. + Required when type is "Secret". + properties: + name: + description: name of the Secret. Must + be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: + lowercase alphanumeric characters, + hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + type: + description: |- + type specifies the source type for the header value. Allowed values: + - "Secret" — reads the value from a Kubernetes Secret (use for + API keys and tokens). Requires the secret field to be set. + - "ServiceAccountToken" — auto-injects a Kubernetes service account token + (for MCP servers that accept K8s auth). + - "Client" — the value is provided by the calling client at + runtime (e.g., forwarded from a user session). + enum: + - Secret + - ServiceAccountToken + - Client + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: secret is required when type is Secret, + and forbidden otherwise + rule: 'self.type == ''Secret'' ? has(self.secret) + : !has(self.secret)' + required: + - name + - valueFrom + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + name: + description: |- + name of the MCP server. Must start with a letter and contain only + lowercase alphanumeric characters and hyphens. Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a lowercase letter and + contain only lowercase alphanumerics and hyphens + rule: self.matches('^[a-z][a-z0-9-]*$') + timeoutSeconds: + default: 5 + description: |- + timeoutSeconds is the per-request timeout for calls to this MCP server, + in seconds. Default is 5. + Valid range: 1-300. + format: int32 + maximum: 300 + minimum: 1 + type: integer + url: + description: |- + url of the MCP server (HTTP/HTTPS). Must be an HTTP or HTTPS URL, + maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: url must be a valid HTTP or HTTPS URL + rule: isURL(self) && url(self).getScheme() in ['http', + 'https'] + required: + - name + - url + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + requiredSecrets: + description: |- + requiredSecrets declares Kubernetes Secrets that the sandbox pod + needs at runtime. The cluster admin creates the actual Secrets + in the same namespace as the Proposal. + items: + description: |- + SecretRequirement declares a Kubernetes Secret that the sandbox needs + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. + properties: + description: + description: |- + description explains what this secret is used for, helping the + cluster admin understand what credentials to provide. + maxLength: 1024 + minLength: 1 + type: string + mountAs: + description: mountAs specifies how the secret is exposed + in the sandbox pod. + properties: + envVar: + description: |- + envVar configures environment variable injection. + Required when type is "EnvVar". + properties: + name: + description: |- + name is the environment variable name (e.g., "GITHUB_TOKEN"). + Must be uppercase letters, digits, and underscores, starting + with a letter or underscore. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid environment variable + name: uppercase letters, digits, and underscores, + starting with a letter or underscore' + rule: self.matches('^[A-Z_][A-Z0-9_]*$') + required: + - name + type: object + filePath: + description: |- + filePath configures file mount. + Required when type is "FilePath". + properties: + path: + description: |- + path is the absolute file path (e.g., "/etc/secrets/tls.crt"). + Must start with a forward slash. + maxLength: 512 + minLength: 2 + type: string + x-kubernetes-validations: + - message: path must be an absolute path starting + with '/' + rule: self.startsWith('/') + required: + - path + type: object + type: + description: |- + type specifies how the secret is exposed. Allowed values: "EnvVar", + "FilePath". + + When set to EnvVar, the secret value is injected as an environment + variable, and the 'envVar' field must be configured. + + When set to FilePath, the secret is mounted as a file, and the + 'filePath' field must be configured. + enum: + - EnvVar + - FilePath + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: envVar is required when type is EnvVar, and + forbidden otherwise + rule: 'self.type == ''EnvVar'' ? has(self.envVar) + : !has(self.envVar)' + - message: filePath is required when type is FilePath, + and forbidden otherwise + rule: 'self.type == ''FilePath'' ? has(self.filePath) + : !has(self.filePath)' + name: + description: |- + name of the Secret (must exist in the operator namespace). + Must be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - mountAs + - name + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + skills: + description: |- + skills defines one or more OCI images containing skills to mount + in the agent's sandbox pod. The operator creates Kubernetes image + volumes (requires K8s 1.34+) and mounts them into the agent's + skills directory. Each image must be unique within the list. + items: + description: "SkillsSource defines an OCI image containing + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" + properties: + image: + description: |- + image is the OCI image reference containing skills. + The operator mounts this as a Kubernetes image volume (requires K8s 1.34+). + Must be a valid OCI image pullspec: a domain, followed by a repository path, + ending with either a tag (:tag) or a digest (@algorithm:hex). + Must be 1-512 characters. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains + must be alphanumeric characters (lowercase and uppercase) + separated by the '.' character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must + contain lowercase alphanumeric characters separated + only by the '.', '_', '__', '-' characters. + rule: self.find('(/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != '' + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != '' || self.find(':.*$') + != '' + - message: tag must not be more than 127 characters + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').substring(1).size() + <= 127 : true) : true' + - message: tag is invalid. valid tags must begin with + a word character followed by word characters, '.', + or '-' + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an alpha character followed by alphanumeric + characters and may contain '-', '_', '+', and '.' + characters. + rule: 'self.find(''(@.*:)'') != '''' ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest must be at least 32 characters + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest must only contain hex characters (A-F, + a-f, 0-9) + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + paths: + description: |- + paths specifies which directories from the image are mounted. + Each path is mounted as a separate subPath volumeMount into the agent's + skills directory. The last segment of each path becomes the mount name + (e.g., "/skills/prometheus" mounts as "prometheus"). + + Each path must be an absolute file path: starts with "/", no ".." + or "." segments, no double slashes, no trailing slash, and only + alphanumeric characters, hyphens, underscores, dots, and slashes. + + Maximum 50 items. + items: + maxLength: 512 + minLength: 2 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each path must be 2-512 characters + rule: self.all(p, p.size() >= 2 && p.size() <= 512) + - message: each path must be absolute (start with '/') + rule: self.all(p, p.startsWith('/')) + - message: paths must not end with '/' + rule: self.all(p, !p.endsWith('/')) + - message: paths must not contain double slashes + rule: self.all(p, !p.contains('//')) + - message: paths must not contain '.' or '..' segments + rule: self.all(p, !p.contains('/../') && !p.endsWith('/..') + && !p.contains('/./') && !p.endsWith('/.')) + - message: paths may only contain alphanumeric characters, + '/', '_', '.', and '-' + rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) + required: + - image + - paths + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - image + x-kubernetes-list-type: map + type: object + type: object + analysisOutput: + description: |- + analysisOutput configures the analysis step's structured output. + The mode field controls which built-in properties are included + (Default: all; Minimal: only title). The schema field optionally + defines adapter-specific structured data injected as "components". + + When omitted, the analysis uses the full default schema with all + built-in properties and no custom components. + + Immutable: the output contract is fixed at creation. + minProperties: 1 + properties: + mode: + default: Default + description: |- + mode controls which built-in properties the analysis output schema + includes. Default includes all built-in properties (diagnosis, + proposal, summary, rbac, verification). Minimal includes only the + base structure (options array with title per option). Omit or set + to "Default" for standard remediation workflows. + enum: + - Default + - Minimal + type: string + schema: + description: |- + schema is a JSON Schema injected as a required "components" + property in each analysis output option. Use this to require + adapter-specific structured data beyond the base analysis schema. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + x-kubernetes-validations: + - message: schema is required when mode is Minimal + rule: self.mode != 'Minimal' || has(self.schema) + execution: + description: |- + execution defines per-step configuration for the execution step. + Omit to skip execution (advisory/assisted patterns). + + Immutable: agent and per-step tools are fixed at creation. + minProperties: 1 + properties: + agent: + description: |- + agent is the name of the cluster-scoped Agent CR to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + tools: + description: |- + tools provides per-step tools that replace the shared spec.tools + for this step. Use this when different steps need different skills. + minProperties: 1 + properties: + mcpServers: + description: |- + mcpServers defines external MCP (Model Context Protocol) servers the + agent can connect to for additional tools and context. + items: + description: "MCPServerConfig defines the configuration + for an MCP (Model Context Protocol)\nserver that the agent + can connect to for additional tools and context.\nMCP + servers extend the agent's capabilities beyond its built-in + skills.\n\nExample — connecting to an OpenShift MCP server + with SA token auth:\n\n\tmcpServers:\n\t - name: openshift\n\t + \ url: https://mcp.openshift-lightspeed.svc:8443/sse\n\t + \ timeoutSeconds: 10\n\t headers:\n\t - name: + Authorization\n\t valueFrom:\n\t type: + ServiceAccountToken\n\nExample — connecting to an external + API with secret-based auth:\n\n\tmcpServers:\n\t - name: + pagerduty\n\t url: https://mcp-pagerduty.example.com/sse\n\t + \ headers:\n\t - name: X-API-Key\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: + pagerduty-api-key" + properties: + headers: + description: headers to send to the MCP server. Maximum + 20 items. + items: + description: |- + MCPHeader defines an HTTP header to send with every request to an + MCP server. Used for authentication and routing. + properties: + name: + description: |- + name of the header (e.g., "Authorization", "X-API-Key"). + Must be at least 1 character, containing only letters, digits, and hyphens. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a letter and contain + only letters, digits, and hyphens + rule: self.matches('^[A-Za-z][A-Za-z0-9-]*$') + valueFrom: + description: valueFrom is the source of the header + value. + properties: + secret: + description: |- + secret references a Secret containing the header value. + Required when type is "Secret". + properties: + name: + description: name of the Secret. Must + be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: + lowercase alphanumeric characters, + hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + type: + description: |- + type specifies the source type for the header value. Allowed values: + - "Secret" — reads the value from a Kubernetes Secret (use for + API keys and tokens). Requires the secret field to be set. + - "ServiceAccountToken" — auto-injects a Kubernetes service account token + (for MCP servers that accept K8s auth). + - "Client" — the value is provided by the calling client at + runtime (e.g., forwarded from a user session). + enum: + - Secret + - ServiceAccountToken + - Client + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: secret is required when type is Secret, + and forbidden otherwise + rule: 'self.type == ''Secret'' ? has(self.secret) + : !has(self.secret)' + required: + - name + - valueFrom + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + name: + description: |- + name of the MCP server. Must start with a letter and contain only + lowercase alphanumeric characters and hyphens. Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a lowercase letter and + contain only lowercase alphanumerics and hyphens + rule: self.matches('^[a-z][a-z0-9-]*$') + timeoutSeconds: + default: 5 + description: |- + timeoutSeconds is the per-request timeout for calls to this MCP server, + in seconds. Default is 5. + Valid range: 1-300. + format: int32 + maximum: 300 + minimum: 1 + type: integer + url: + description: |- + url of the MCP server (HTTP/HTTPS). Must be an HTTP or HTTPS URL, + maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: url must be a valid HTTP or HTTPS URL + rule: isURL(self) && url(self).getScheme() in ['http', + 'https'] + required: + - name + - url + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + requiredSecrets: + description: |- + requiredSecrets declares Kubernetes Secrets that the sandbox pod + needs at runtime. The cluster admin creates the actual Secrets + in the same namespace as the Proposal. + items: + description: |- + SecretRequirement declares a Kubernetes Secret that the sandbox needs + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. + properties: + description: + description: |- + description explains what this secret is used for, helping the + cluster admin understand what credentials to provide. + maxLength: 1024 + minLength: 1 + type: string + mountAs: + description: mountAs specifies how the secret is exposed + in the sandbox pod. + properties: + envVar: + description: |- + envVar configures environment variable injection. + Required when type is "EnvVar". + properties: + name: + description: |- + name is the environment variable name (e.g., "GITHUB_TOKEN"). + Must be uppercase letters, digits, and underscores, starting + with a letter or underscore. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid environment variable + name: uppercase letters, digits, and underscores, + starting with a letter or underscore' + rule: self.matches('^[A-Z_][A-Z0-9_]*$') + required: + - name + type: object + filePath: + description: |- + filePath configures file mount. + Required when type is "FilePath". + properties: + path: + description: |- + path is the absolute file path (e.g., "/etc/secrets/tls.crt"). + Must start with a forward slash. + maxLength: 512 + minLength: 2 + type: string + x-kubernetes-validations: + - message: path must be an absolute path starting + with '/' + rule: self.startsWith('/') + required: + - path + type: object + type: + description: |- + type specifies how the secret is exposed. Allowed values: "EnvVar", + "FilePath". + + When set to EnvVar, the secret value is injected as an environment + variable, and the 'envVar' field must be configured. + + When set to FilePath, the secret is mounted as a file, and the + 'filePath' field must be configured. + enum: + - EnvVar + - FilePath + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: envVar is required when type is EnvVar, and + forbidden otherwise + rule: 'self.type == ''EnvVar'' ? has(self.envVar) + : !has(self.envVar)' + - message: filePath is required when type is FilePath, + and forbidden otherwise + rule: 'self.type == ''FilePath'' ? has(self.filePath) + : !has(self.filePath)' + name: + description: |- + name of the Secret (must exist in the operator namespace). + Must be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - mountAs + - name + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + skills: + description: |- + skills defines one or more OCI images containing skills to mount + in the agent's sandbox pod. The operator creates Kubernetes image + volumes (requires K8s 1.34+) and mounts them into the agent's + skills directory. Each image must be unique within the list. + items: + description: "SkillsSource defines an OCI image containing + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" + properties: + image: + description: |- + image is the OCI image reference containing skills. + The operator mounts this as a Kubernetes image volume (requires K8s 1.34+). + Must be a valid OCI image pullspec: a domain, followed by a repository path, + ending with either a tag (:tag) or a digest (@algorithm:hex). + Must be 1-512 characters. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains + must be alphanumeric characters (lowercase and uppercase) + separated by the '.' character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must + contain lowercase alphanumeric characters separated + only by the '.', '_', '__', '-' characters. + rule: self.find('(/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != '' + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != '' || self.find(':.*$') + != '' + - message: tag must not be more than 127 characters + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').substring(1).size() + <= 127 : true) : true' + - message: tag is invalid. valid tags must begin with + a word character followed by word characters, '.', + or '-' + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an alpha character followed by alphanumeric + characters and may contain '-', '_', '+', and '.' + characters. + rule: 'self.find(''(@.*:)'') != '''' ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest must be at least 32 characters + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest must only contain hex characters (A-F, + a-f, 0-9) + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + paths: + description: |- + paths specifies which directories from the image are mounted. + Each path is mounted as a separate subPath volumeMount into the agent's + skills directory. The last segment of each path becomes the mount name + (e.g., "/skills/prometheus" mounts as "prometheus"). + + Each path must be an absolute file path: starts with "/", no ".." + or "." segments, no double slashes, no trailing slash, and only + alphanumeric characters, hyphens, underscores, dots, and slashes. + + Maximum 50 items. + items: + maxLength: 512 + minLength: 2 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each path must be 2-512 characters + rule: self.all(p, p.size() >= 2 && p.size() <= 512) + - message: each path must be absolute (start with '/') + rule: self.all(p, p.startsWith('/')) + - message: paths must not end with '/' + rule: self.all(p, !p.endsWith('/')) + - message: paths must not contain double slashes + rule: self.all(p, !p.contains('//')) + - message: paths must not contain '.' or '..' segments + rule: self.all(p, !p.contains('/../') && !p.endsWith('/..') + && !p.contains('/./') && !p.endsWith('/.')) + - message: paths may only contain alphanumeric characters, + '/', '_', '.', and '-' + rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) + required: + - image + - paths + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - image + x-kubernetes-list-type: map + type: object + type: object + request: + description: |- + request is the user's original request, alert description, or a + description of what triggered this proposal. This text is passed to + the analysis agent as the primary input. + + Immutable: Proposals are run-to-completion (like Jobs). To change + the request, create a new Proposal. Use spec.revisionFeedback for + iterative feedback on an existing analysis. + maxLength: 32768 + minLength: 1 + type: string + x-kubernetes-validations: + - message: request is immutable after creation + rule: self == oldSelf + revisionFeedback: + description: |- + revisionFeedback is the user's free-text feedback requesting changes + to the analysis. Patching this field bumps metadata.generation, which + the operator detects (generation > observedGeneration) and triggers + re-analysis with the feedback appended to the original request. + + Mutable: this is the only mutable spec field. All other spec fields + are immutable via CEL rules, so generation changes signal revision. + maxLength: 32768 + minLength: 1 + type: string + targetNamespaces: + description: |- + targetNamespaces are the Kubernetes namespace(s) this proposal + operates on. Used for RBAC scoping and context to the analysis agent. + + When omitted, the proposal is not namespace-scoped — the analysis + agent determines the relevant namespaces from the request context. + Adapters (AlertManager, ACS) typically set this automatically from + the source event. + + Immutable: RBAC scoping is fixed at creation. Changing target + namespaces mid-flight would invalidate the analysis and any + granted execution RBAC. + items: + maxLength: 63 + minLength: 1 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each namespace must be a valid DNS label + rule: self.all(ns, !format.dns1123Label().validate(ns).hasValue()) + tools: + description: |- + tools defines the default tools for all steps: skills images, + MCP servers, and required secrets. Per-step tools + (analysis.tools, execution.tools, verification.tools) replace + this default for individual steps. + + Immutable: the skills and secrets available to the agent are + fixed at creation. Changing tools mid-flight could violate the + assumptions of an in-progress analysis or execution. + minProperties: 1 + properties: + mcpServers: + description: |- + mcpServers defines external MCP (Model Context Protocol) servers the + agent can connect to for additional tools and context. + items: + description: "MCPServerConfig defines the configuration for + an MCP (Model Context Protocol)\nserver that the agent can + connect to for additional tools and context.\nMCP servers + extend the agent's capabilities beyond its built-in skills.\n\nExample + — connecting to an OpenShift MCP server with SA token auth:\n\n\tmcpServers:\n\t + \ - name: openshift\n\t url: https://mcp.openshift-lightspeed.svc:8443/sse\n\t + \ timeoutSeconds: 10\n\t headers:\n\t - name: Authorization\n\t + \ valueFrom:\n\t type: ServiceAccountToken\n\nExample + — connecting to an external API with secret-based auth:\n\n\tmcpServers:\n\t + \ - name: pagerduty\n\t url: https://mcp-pagerduty.example.com/sse\n\t + \ headers:\n\t - name: X-API-Key\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: + pagerduty-api-key" + properties: + headers: + description: headers to send to the MCP server. Maximum + 20 items. + items: + description: |- + MCPHeader defines an HTTP header to send with every request to an + MCP server. Used for authentication and routing. + properties: + name: + description: |- + name of the header (e.g., "Authorization", "X-API-Key"). + Must be at least 1 character, containing only letters, digits, and hyphens. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a letter and contain + only letters, digits, and hyphens + rule: self.matches('^[A-Za-z][A-Za-z0-9-]*$') + valueFrom: + description: valueFrom is the source of the header + value. + properties: + secret: + description: |- + secret references a Secret containing the header value. + Required when type is "Secret". + properties: + name: + description: name of the Secret. Must be a + valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: + lowercase alphanumeric characters, hyphens, + and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + type: + description: |- + type specifies the source type for the header value. Allowed values: + - "Secret" — reads the value from a Kubernetes Secret (use for + API keys and tokens). Requires the secret field to be set. + - "ServiceAccountToken" — auto-injects a Kubernetes service account token + (for MCP servers that accept K8s auth). + - "Client" — the value is provided by the calling client at + runtime (e.g., forwarded from a user session). + enum: + - Secret + - ServiceAccountToken + - Client + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: secret is required when type is Secret, + and forbidden otherwise + rule: 'self.type == ''Secret'' ? has(self.secret) + : !has(self.secret)' + required: + - name + - valueFrom + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + name: + description: |- + name of the MCP server. Must start with a letter and contain only + lowercase alphanumeric characters and hyphens. Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a lowercase letter and contain + only lowercase alphanumerics and hyphens + rule: self.matches('^[a-z][a-z0-9-]*$') + timeoutSeconds: + default: 5 + description: |- + timeoutSeconds is the per-request timeout for calls to this MCP server, + in seconds. Default is 5. + Valid range: 1-300. + format: int32 + maximum: 300 + minimum: 1 + type: integer + url: + description: |- + url of the MCP server (HTTP/HTTPS). Must be an HTTP or HTTPS URL, + maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: url must be a valid HTTP or HTTPS URL + rule: isURL(self) && url(self).getScheme() in ['http', + 'https'] + required: + - name + - url + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + requiredSecrets: + description: |- + requiredSecrets declares Kubernetes Secrets that the sandbox pod + needs at runtime. The cluster admin creates the actual Secrets + in the same namespace as the Proposal. + items: + description: |- + SecretRequirement declares a Kubernetes Secret that the sandbox needs + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. + properties: + description: + description: |- + description explains what this secret is used for, helping the + cluster admin understand what credentials to provide. + maxLength: 1024 + minLength: 1 + type: string + mountAs: + description: mountAs specifies how the secret is exposed + in the sandbox pod. + properties: + envVar: + description: |- + envVar configures environment variable injection. + Required when type is "EnvVar". + properties: + name: + description: |- + name is the environment variable name (e.g., "GITHUB_TOKEN"). + Must be uppercase letters, digits, and underscores, starting + with a letter or underscore. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid environment variable + name: uppercase letters, digits, and underscores, + starting with a letter or underscore' + rule: self.matches('^[A-Z_][A-Z0-9_]*$') + required: + - name + type: object + filePath: + description: |- + filePath configures file mount. + Required when type is "FilePath". + properties: + path: + description: |- + path is the absolute file path (e.g., "/etc/secrets/tls.crt"). + Must start with a forward slash. + maxLength: 512 + minLength: 2 + type: string + x-kubernetes-validations: + - message: path must be an absolute path starting + with '/' + rule: self.startsWith('/') + required: + - path + type: object + type: + description: |- + type specifies how the secret is exposed. Allowed values: "EnvVar", + "FilePath". + + When set to EnvVar, the secret value is injected as an environment + variable, and the 'envVar' field must be configured. + + When set to FilePath, the secret is mounted as a file, and the + 'filePath' field must be configured. + enum: + - EnvVar + - FilePath + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: envVar is required when type is EnvVar, and forbidden + otherwise + rule: 'self.type == ''EnvVar'' ? has(self.envVar) : !has(self.envVar)' + - message: filePath is required when type is FilePath, and + forbidden otherwise + rule: 'self.type == ''FilePath'' ? has(self.filePath) + : !has(self.filePath)' + name: + description: |- + name of the Secret (must exist in the operator namespace). + Must be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - mountAs + - name + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + skills: + description: |- + skills defines one or more OCI images containing skills to mount + in the agent's sandbox pod. The operator creates Kubernetes image + volumes (requires K8s 1.34+) and mounts them into the agent's + skills directory. Each image must be unique within the list. + items: + description: "SkillsSource defines an OCI image containing skills + and which paths\nwithin that image to mount. Skills are mounted + as Kubernetes image\nvolumes in the agent's sandbox pod.\n\nEach + path is mounted as a separate subPath volumeMount, allowing\nselective + composition of skills from shared images.\n\nExample — mount + specific skills from the agentic-skills image:\n\n\tskills:\n\t + \ - image: registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t + \ paths:\n\t - /skills/cluster-update/update-advisor" + properties: + image: + description: |- + image is the OCI image reference containing skills. + The operator mounts this as a Kubernetes image volume (requires K8s 1.34+). + Must be a valid OCI image pullspec: a domain, followed by a repository path, + ending with either a tag (:tag) or a digest (@algorithm:hex). + Must be 1-512 characters. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains + must be alphanumeric characters (lowercase and uppercase) + separated by the '.' character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by + the '.', '_', '__', '-' characters. + rule: self.find('(/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != '' + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != '' || self.find(':.*$') != + '' + - message: tag must not be more than 127 characters + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').substring(1).size() <= + 127 : true) : true' + - message: tag is invalid. valid tags must begin with a + word character followed by word characters, '.', or + '-' + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an alpha character followed by alphanumeric + characters and may contain '-', '_', '+', and '.' characters. + rule: 'self.find(''(@.*:)'') != '''' ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest must be at least 32 characters + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest must only contain hex characters (A-F, + a-f, 0-9) + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + paths: + description: |- + paths specifies which directories from the image are mounted. + Each path is mounted as a separate subPath volumeMount into the agent's + skills directory. The last segment of each path becomes the mount name + (e.g., "/skills/prometheus" mounts as "prometheus"). + + Each path must be an absolute file path: starts with "/", no ".." + or "." segments, no double slashes, no trailing slash, and only + alphanumeric characters, hyphens, underscores, dots, and slashes. + + Maximum 50 items. + items: + maxLength: 512 + minLength: 2 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each path must be 2-512 characters + rule: self.all(p, p.size() >= 2 && p.size() <= 512) + - message: each path must be absolute (start with '/') + rule: self.all(p, p.startsWith('/')) + - message: paths must not end with '/' + rule: self.all(p, !p.endsWith('/')) + - message: paths must not contain double slashes + rule: self.all(p, !p.contains('//')) + - message: paths must not contain '.' or '..' segments + rule: self.all(p, !p.contains('/../') && !p.endsWith('/..') + && !p.contains('/./') && !p.endsWith('/.')) + - message: paths may only contain alphanumeric characters, + '/', '_', '.', and '-' + rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) + required: + - image + - paths + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - image + x-kubernetes-list-type: map + type: object + verification: + description: |- + verification defines per-step configuration for the verification step. + Omit to skip verification. + + Immutable: agent and per-step tools are fixed at creation. + minProperties: 1 + properties: + agent: + description: |- + agent is the name of the cluster-scoped Agent CR to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase alphanumeric + characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + tools: + description: |- + tools provides per-step tools that replace the shared spec.tools + for this step. Use this when different steps need different skills. + minProperties: 1 + properties: + mcpServers: + description: |- + mcpServers defines external MCP (Model Context Protocol) servers the + agent can connect to for additional tools and context. + items: + description: "MCPServerConfig defines the configuration + for an MCP (Model Context Protocol)\nserver that the agent + can connect to for additional tools and context.\nMCP + servers extend the agent's capabilities beyond its built-in + skills.\n\nExample — connecting to an OpenShift MCP server + with SA token auth:\n\n\tmcpServers:\n\t - name: openshift\n\t + \ url: https://mcp.openshift-lightspeed.svc:8443/sse\n\t + \ timeoutSeconds: 10\n\t headers:\n\t - name: + Authorization\n\t valueFrom:\n\t type: + ServiceAccountToken\n\nExample — connecting to an external + API with secret-based auth:\n\n\tmcpServers:\n\t - name: + pagerduty\n\t url: https://mcp-pagerduty.example.com/sse\n\t + \ headers:\n\t - name: X-API-Key\n\t valueFrom:\n\t + \ type: Secret\n\t secret:\n\t name: + pagerduty-api-key" + properties: + headers: + description: headers to send to the MCP server. Maximum + 20 items. + items: + description: |- + MCPHeader defines an HTTP header to send with every request to an + MCP server. Used for authentication and routing. + properties: + name: + description: |- + name of the header (e.g., "Authorization", "X-API-Key"). + Must be at least 1 character, containing only letters, digits, and hyphens. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a letter and contain + only letters, digits, and hyphens + rule: self.matches('^[A-Za-z][A-Za-z0-9-]*$') + valueFrom: + description: valueFrom is the source of the header + value. + properties: + secret: + description: |- + secret references a Secret containing the header value. + Required when type is "Secret". + properties: + name: + description: name of the Secret. Must + be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: + lowercase alphanumeric characters, + hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - name + type: object + type: + description: |- + type specifies the source type for the header value. Allowed values: + - "Secret" — reads the value from a Kubernetes Secret (use for + API keys and tokens). Requires the secret field to be set. + - "ServiceAccountToken" — auto-injects a Kubernetes service account token + (for MCP servers that accept K8s auth). + - "Client" — the value is provided by the calling client at + runtime (e.g., forwarded from a user session). + enum: + - Secret + - ServiceAccountToken + - Client + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: secret is required when type is Secret, + and forbidden otherwise + rule: 'self.type == ''Secret'' ? has(self.secret) + : !has(self.secret)' + required: + - name + - valueFrom + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + name: + description: |- + name of the MCP server. Must start with a letter and contain only + lowercase alphanumeric characters and hyphens. Must be 1-253 characters. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: name must start with a lowercase letter and + contain only lowercase alphanumerics and hyphens + rule: self.matches('^[a-z][a-z0-9-]*$') + timeoutSeconds: + default: 5 + description: |- + timeoutSeconds is the per-request timeout for calls to this MCP server, + in seconds. Default is 5. + Valid range: 1-300. + format: int32 + maximum: 300 + minimum: 1 + type: integer + url: + description: |- + url of the MCP server (HTTP/HTTPS). Must be an HTTP or HTTPS URL, + maximum 2048 characters. + maxLength: 2048 + minLength: 1 + type: string + x-kubernetes-validations: + - message: url must be a valid HTTP or HTTPS URL + rule: isURL(self) && url(self).getScheme() in ['http', + 'https'] + required: + - name + - url + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + requiredSecrets: + description: |- + requiredSecrets declares Kubernetes Secrets that the sandbox pod + needs at runtime. The cluster admin creates the actual Secrets + in the same namespace as the Proposal. + items: + description: |- + SecretRequirement declares a Kubernetes Secret that the sandbox needs + at runtime. The Secret must exist in the operator namespace (where + sandbox pods run), not in the Proposal's namespace. + properties: + description: + description: |- + description explains what this secret is used for, helping the + cluster admin understand what credentials to provide. + maxLength: 1024 + minLength: 1 + type: string + mountAs: + description: mountAs specifies how the secret is exposed + in the sandbox pod. + properties: + envVar: + description: |- + envVar configures environment variable injection. + Required when type is "EnvVar". + properties: + name: + description: |- + name is the environment variable name (e.g., "GITHUB_TOKEN"). + Must be uppercase letters, digits, and underscores, starting + with a letter or underscore. + maxLength: 256 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid environment variable + name: uppercase letters, digits, and underscores, + starting with a letter or underscore' + rule: self.matches('^[A-Z_][A-Z0-9_]*$') + required: + - name + type: object + filePath: + description: |- + filePath configures file mount. + Required when type is "FilePath". + properties: + path: + description: |- + path is the absolute file path (e.g., "/etc/secrets/tls.crt"). + Must start with a forward slash. + maxLength: 512 + minLength: 2 + type: string + x-kubernetes-validations: + - message: path must be an absolute path starting + with '/' + rule: self.startsWith('/') + required: + - path + type: object + type: + description: |- + type specifies how the secret is exposed. Allowed values: "EnvVar", + "FilePath". + + When set to EnvVar, the secret value is injected as an environment + variable, and the 'envVar' field must be configured. + + When set to FilePath, the secret is mounted as a file, and the + 'filePath' field must be configured. + enum: + - EnvVar + - FilePath + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: envVar is required when type is EnvVar, and + forbidden otherwise + rule: 'self.type == ''EnvVar'' ? has(self.envVar) + : !has(self.envVar)' + - message: filePath is required when type is FilePath, + and forbidden otherwise + rule: 'self.type == ''FilePath'' ? has(self.filePath) + : !has(self.filePath)' + name: + description: |- + name of the Secret (must exist in the operator namespace). + Must be a valid RFC 1123 DNS subdomain. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS subdomain: lowercase + alphanumeric characters, hyphens, and dots' + rule: '!format.dns1123Subdomain().validate(self).hasValue()' + required: + - mountAs + - name + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + skills: + description: |- + skills defines one or more OCI images containing skills to mount + in the agent's sandbox pod. The operator creates Kubernetes image + volumes (requires K8s 1.34+) and mounts them into the agent's + skills directory. Each image must be unique within the list. + items: + description: "SkillsSource defines an OCI image containing + skills and which paths\nwithin that image to mount. Skills + are mounted as Kubernetes image\nvolumes in the agent's + sandbox pod.\n\nEach path is mounted as a separate subPath + volumeMount, allowing\nselective composition of skills + from shared images.\n\nExample — mount specific skills + from the agentic-skills image:\n\n\tskills:\n\t - image: + registry.ci.openshift.org/ocp/5.0:agentic-skills\n\t paths:\n\t + \ - /skills/cluster-update/update-advisor" + properties: + image: + description: |- + image is the OCI image reference containing skills. + The operator mounts this as a Kubernetes image volume (requires K8s 1.34+). + Must be a valid OCI image pullspec: a domain, followed by a repository path, + ending with either a tag (:tag) or a digest (@algorithm:hex). + Must be 1-512 characters. + maxLength: 512 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains + must be alphanumeric characters (lowercase and uppercase) + separated by the '.' character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must + contain lowercase alphanumeric characters separated + only by the '.', '_', '__', '-' characters. + rule: self.find('(/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != '' + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != '' || self.find(':.*$') + != '' + - message: tag must not be more than 127 characters + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').substring(1).size() + <= 127 : true) : true' + - message: tag is invalid. valid tags must begin with + a word character followed by word characters, '.', + or '-' + rule: 'self.find(''(@.*:)'') == '''' ? (self.find('':.*$'') + != '''' ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an alpha character followed by alphanumeric + characters and may contain '-', '_', '+', and '.' + characters. + rule: 'self.find(''(@.*:)'') != '''' ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest must be at least 32 characters + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest must only contain hex characters (A-F, + a-f, 0-9) + rule: 'self.find(''(@.*:)'') != '''' ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + paths: + description: |- + paths specifies which directories from the image are mounted. + Each path is mounted as a separate subPath volumeMount into the agent's + skills directory. The last segment of each path becomes the mount name + (e.g., "/skills/prometheus" mounts as "prometheus"). + + Each path must be an absolute file path: starts with "/", no ".." + or "." segments, no double slashes, no trailing slash, and only + alphanumeric characters, hyphens, underscores, dots, and slashes. + + Maximum 50 items. + items: + maxLength: 512 + minLength: 2 + type: string + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: each path must be 2-512 characters + rule: self.all(p, p.size() >= 2 && p.size() <= 512) + - message: each path must be absolute (start with '/') + rule: self.all(p, p.startsWith('/')) + - message: paths must not end with '/' + rule: self.all(p, !p.endsWith('/')) + - message: paths must not contain double slashes + rule: self.all(p, !p.contains('//')) + - message: paths must not contain '.' or '..' segments + rule: self.all(p, !p.contains('/../') && !p.endsWith('/..') + && !p.contains('/./') && !p.endsWith('/.')) + - message: paths may only contain alphanumeric characters, + '/', '_', '.', and '-' + rule: self.all(p, p.matches('^[a-zA-Z0-9/_.-]+$')) + required: + - image + - paths + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - image + x-kubernetes-list-type: map + type: object + type: object + required: + - analysis + - request + type: object + x-kubernetes-validations: + - message: analysis must be provided + rule: has(self.analysis) + - message: targetNamespaces is immutable once set + rule: '!has(oldSelf.targetNamespaces) || (has(self.targetNamespaces) + && self.targetNamespaces == oldSelf.targetNamespaces)' + - message: analysisOutput is immutable once set + rule: '!has(oldSelf.analysisOutput) || (has(self.analysisOutput) && + self.analysisOutput == oldSelf.analysisOutput)' + - message: analysisOutput mode Minimal is only allowed for analysis-only + proposals (no execution or verification steps) + rule: '!has(self.analysisOutput) || self.analysisOutput.mode != ''Minimal'' + || (!has(self.execution) && !has(self.verification))' + - message: tools is immutable once set + rule: '!has(oldSelf.tools) || (has(self.tools) && self.tools == oldSelf.tools)' + - message: analysis is immutable once set + rule: '!has(oldSelf.analysis) || (has(self.analysis) && self.analysis + == oldSelf.analysis)' + - message: execution is immutable once set + rule: '!has(oldSelf.execution) || (has(self.execution) && self.execution + == oldSelf.execution)' + - message: verification is immutable once set + rule: '!has(oldSelf.verification) || (has(self.verification) && self.verification + == oldSelf.verification)' + status: + description: status defines the observed state of Proposal. + minProperties: 1 + properties: + conditions: + description: |- + conditions represent the latest available observations using the + standard Kubernetes condition pattern. Condition types include: + Analyzed, Approved, Executed, Verified, and Escalated. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + steps: + description: |- + steps contains the per-step observed state (analysis, execution, + verification). Each step independently tracks its timing, sandbox + info, and references to result CRs. + minProperties: 1 + properties: + analysis: + description: analysis is the observed state of the analysis step. + minProperties: 1 + properties: + conditions: + description: conditions for this step. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + results: + description: |- + results references AnalysisResult CRs, newest last. + Each entry corresponds to one analysis attempt. + items: + description: |- + StepResultRef is a lightweight reference to a result CR with an inline + success field for quick scanning without fetching the CR. + properties: + name: + description: name is the name of the result CR. + maxLength: 253 + minLength: 1 + type: string + outcome: + description: |- + outcome indicates the result of this step attempt. + Must be one of: Succeeded, Failed. + enum: + - Succeeded + - Failed + type: string + required: + - name + - outcome + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + sandbox: + description: sandbox tracks the sandbox used. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + type: object + escalation: + description: escalation is the observed state of the escalation + step. + minProperties: 1 + properties: + conditions: + description: conditions for this step. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + results: + description: results references EscalationResult CRs, newest + last. + items: + description: |- + StepResultRef is a lightweight reference to a result CR with an inline + success field for quick scanning without fetching the CR. + properties: + name: + description: name is the name of the result CR. + maxLength: 253 + minLength: 1 + type: string + outcome: + description: |- + outcome indicates the result of this step attempt. + Must be one of: Succeeded, Failed. + enum: + - Succeeded + - Failed + type: string + required: + - name + - outcome + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + sandbox: + description: sandbox tracks the sandbox used. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + type: object + execution: + description: execution is the observed state of the execution + step. + minProperties: 1 + properties: + conditions: + description: conditions for this step. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + results: + description: |- + results references ExecutionResult CRs, newest last. + Each entry corresponds to one execution attempt (including retries). + items: + description: |- + StepResultRef is a lightweight reference to a result CR with an inline + success field for quick scanning without fetching the CR. + properties: + name: + description: name is the name of the result CR. + maxLength: 253 + minLength: 1 + type: string + outcome: + description: |- + outcome indicates the result of this step attempt. + Must be one of: Succeeded, Failed. + enum: + - Succeeded + - Failed + type: string + required: + - name + - outcome + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + retryCount: + description: |- + retryCount tracks how many times execution+verification has been + retried for the current analysis option. Reset when a new analysis + is run (initial or revision). The operator increments this on each + objective verification failure before retrying execution. + format: int32 + minimum: 0 + type: integer + sandbox: + description: sandbox tracks the sandbox used. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + type: object + verification: + description: verification is the observed state of the verification + step. + minProperties: 1 + properties: + conditions: + description: conditions for this step. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + results: + description: |- + results references VerificationResult CRs, newest last. + Each entry corresponds to one verification attempt (including retries). + items: + description: |- + StepResultRef is a lightweight reference to a result CR with an inline + success field for quick scanning without fetching the CR. + properties: + name: + description: name is the name of the result CR. + maxLength: 253 + minLength: 1 + type: string + outcome: + description: |- + outcome indicates the result of this step attempt. + Must be one of: Succeeded, Failed. + enum: + - Succeeded + - Failed + type: string + required: + - name + - outcome + type: object + maxItems: 20 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + sandbox: + description: sandbox tracks the sandbox used. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic + character and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + type: object + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_proposaltemplates.yaml b/config/crd/bases/agentic.openshift.io_proposaltemplates.yaml new file mode 100644 index 000000000..8096d6a29 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_proposaltemplates.yaml @@ -0,0 +1,124 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: proposaltemplates.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: ProposalTemplate + listKind: ProposalTemplateList + plural: proposaltemplates + singular: proposaltemplate + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.analysis.agent + name: Analysis + type: string + - jsonPath: .spec.execution.agent + name: Execution + type: string + - jsonPath: .spec.verification.agent + name: Verification + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "ProposalTemplate defines a reusable workflow shape that controls + which\nagent tier handles analysis, execution, and verification. It is\ncluster-scoped + and created by the cluster admin.\n\nExample — advisory (analysis only):\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: ProposalTemplate\n\tmetadata:\n\t + \ name: advisory\n\tspec:\n\t analysis:\n\t agent: smart\n\nExample + — remediation (full pipeline):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + ProposalTemplate\n\tmetadata:\n\t name: remediation\n\tspec:\n\t maxAttempts: + 3\n\t analysis:\n\t agent: smart\n\t execution: {}\n\t verification:\n\t + \ agent: fast\n\nExample — assisted (analysis + verification, no execution):\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: ProposalTemplate\n\tmetadata:\n\t + \ name: assisted\n\tspec:\n\t analysis:\n\t agent: smart\n\t verification:\n\t + \ agent: fast" + 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: spec defines the desired state of ProposalTemplate. + properties: + analysis: + description: |- + analysis defines the analysis step. The analysis agent examines + cluster state, produces a diagnosis, remediation proposal, + verification plan, and RBAC permissions needed for execution. + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + type: object + execution: + description: |- + execution defines the execution step. When omitted, the proposal + transitions to AwaitingSync after approval (advisory/assisted patterns). + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + type: object + maxAttempts: + description: maxAttempts is the default retry limit for proposals + using this template. + format: int32 + maximum: 20 + minimum: 0 + type: integer + verification: + description: |- + verification defines the verification step. When omitted, the + proposal completes immediately after execution without verification. + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent to use for this step. + Defaults to "default" when omitted. + maxLength: 253 + minLength: 1 + type: string + type: object + required: + - analysis + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/config/crd/bases/agentic.openshift.io_verificationresults.yaml b/config/crd/bases/agentic.openshift.io_verificationresults.yaml new file mode 100644 index 000000000..af892bdd6 --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_verificationresults.yaml @@ -0,0 +1,235 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: verificationresults.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: VerificationResult + listKind: VerificationResultList + plural: verificationresults + singular: verificationresult + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.proposalName + name: Proposal + type: string + - jsonPath: .spec.retryIndex + name: Retry + type: integer + - jsonPath: .status.conditions[?(@.type=="Completed")].reason + name: Outcome + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + VerificationResult records the output of a single verification step + execution. Created by the operator after the verification agent + completes. Owned by the parent Proposal for garbage collection. + 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: spec contains the immutable identity fields for this result. + properties: + proposalName: + description: proposalName is the name of the parent Proposal in the + same namespace. + maxLength: 253 + minLength: 1 + type: string + retryIndex: + description: retryIndex is the 0-based retry index within the current + analysis. + format: int32 + maximum: 2 + minimum: 0 + type: integer + required: + - proposalName + - retryIndex + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + description: status contains result data and conditions. + minProperties: 1 + properties: + checks: + description: checks contains individual verification check results. + items: + description: |- + VerifyCheck is a single verification check result from the verification + agent. Each check corresponds to a VerificationStep from the analysis + agent's verification plan. + properties: + name: + description: |- + name is the check identifier, matching the VerificationStep name. + Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + result: + description: |- + result indicates whether the check's observed value matches + the expected value. Must be one of: Passed, Failed. + enum: + - Passed + - Failed + type: string + source: + description: |- + source is what performed the check (e.g., "oc", "promql", "curl"). + Maximum 256 characters. + maxLength: 256 + minLength: 1 + type: string + value: + description: |- + value is the actual observed value (e.g., "Running", "3 replicas"). + Maximum 4096 characters. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - result + - source + - value + type: object + maxItems: 50 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + conditions: + description: conditions track the lifecycle of this result. + 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 + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + failureReason: + description: failureReason is populated when the step failed due to + a system error. + maxLength: 8192 + minLength: 1 + type: string + sandbox: + description: sandbox tracks the sandbox pod used for this verification. + properties: + claimName: + description: |- + claimName is the name of the SandboxClaim resource that owns the + sandbox pod. Maximum 253 characters. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + namespace is the namespace where the SandboxClaim and its pod live. + Must be a valid RFC 1123 DNS label. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: 'must be a valid DNS label: lowercase alphanumeric + characters and hyphens, starting with an alphabetic character + and ending with an alphanumeric character' + rule: '!format.dns1123Label().validate(self).hasValue()' + required: + - claimName + - namespace + type: object + summary: + description: summary is a Markdown-formatted verification summary. + maxLength: 32768 + minLength: 1 + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/agentic.openshift.io_workflows.yaml b/config/crd/bases/agentic.openshift.io_workflows.yaml new file mode 100644 index 000000000..a59d0634e --- /dev/null +++ b/config/crd/bases/agentic.openshift.io_workflows.yaml @@ -0,0 +1,192 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: workflows.agentic.openshift.io +spec: + group: agentic.openshift.io + names: + kind: Workflow + listKind: WorkflowList + plural: workflows + singular: workflow + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.analysis.componentTools.name + name: Analysis + type: string + - jsonPath: .spec.execution.componentTools.name + name: Execution + type: string + - jsonPath: .spec.verification.componentTools.name + name: Verification + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: "Workflow defines a reusable 3-step pipeline template that controls + which\nagent tier and component tools handle analysis, execution, and verification.\nIt + is owned by the component team and lives in their namespace alongside\nComponentTools + and Proposals.\n\nWorkflow is namespace-scoped. You create workflows representing + different\noperational patterns and then reference them from proposals. + Per-proposal\noverrides (WorkflowOverride in the Proposal spec) allow swapping + agents\nor component tools for individual steps without creating a new Workflow.\n\nExample + — full remediation (analyze, execute, verify):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Workflow\n\tmetadata:\n\t name: remediation\n\tspec:\n\t analysis:\n\t + \ agent: smart\n\t componentTools:\n\t name: my-tools\n\t execution:\n\t + \ componentTools:\n\t name: my-tools\n\t verification:\n\t agent: + fast\n\t componentTools:\n\t name: my-tools\n\nExample — advisory-only + (analyze only, no execution or verification):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Workflow\n\tmetadata:\n\t name: advisory-only\n\tspec:\n\t analysis:\n\t + \ componentTools:\n\t name: my-tools\n\nExample — gitops (analyze, + skip execution, verify after user applies via git):\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + Workflow\n\tmetadata:\n\t name: gitops-remediation\n\tspec:\n\t analysis:\n\t + \ componentTools:\n\t name: my-tools\n\t verification:\n\t componentTools:\n\t + \ name: my-tools\n\nExample — trust-mode (analyze, execute, skip verification):\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: Workflow\n\tmetadata:\n\t name: + trust-mode\n\tspec:\n\t analysis:\n\t componentTools:\n\t name: + my-tools\n\t execution:\n\t componentTools:\n\t name: my-tools" + 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: spec defines the desired state of Workflow. + properties: + analysis: + description: |- + analysis defines the analysis step configuration. The analysis + agent examines the cluster state, produces a diagnosis (root cause, + confidence), a remediation proposal (actions, risk, reversibility), + a verification plan, and RBAC permissions needed for execution. + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent (tier) to use for this step. + Defaults to "default" when omitted. The cluster admin creates Agent + resources (e.g., "default", "smart", "fast"); the component owner + references them by name here. + maxLength: 253 + minLength: 1 + type: string + componentTools: + description: |- + componentTools references the ComponentTools CR (in the same namespace + as the Workflow) that provides skills, MCP servers, system prompt, and + output schema for this step. + properties: + name: + description: name of the ComponentTools. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + required: + - componentTools + type: object + execution: + description: |- + execution defines the execution step configuration. The execution + agent carries out the approved remediation plan using the RBAC + permissions granted by the operator. + + When omitted (nil), the proposal transitions to AwaitingSync after + approval, making it advisory-only. The user is expected to apply + changes manually or via GitOps. + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent (tier) to use for this step. + Defaults to "default" when omitted. The cluster admin creates Agent + resources (e.g., "default", "smart", "fast"); the component owner + references them by name here. + maxLength: 253 + minLength: 1 + type: string + componentTools: + description: |- + componentTools references the ComponentTools CR (in the same namespace + as the Workflow) that provides skills, MCP servers, system prompt, and + output schema for this step. + properties: + name: + description: name of the ComponentTools. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + required: + - componentTools + type: object + verification: + description: |- + verification defines the verification step configuration. The + verification agent checks whether the remediation was successful + by running the verification plan produced during analysis. + + When omitted (nil), the proposal completes immediately after execution + without a verification check. Useful for trust-mode workflows where + the execution agent's inline verification is sufficient. + properties: + agent: + default: default + description: |- + agent is the name of the cluster-scoped Agent (tier) to use for this step. + Defaults to "default" when omitted. The cluster admin creates Agent + resources (e.g., "default", "smart", "fast"); the component owner + references them by name here. + maxLength: 253 + minLength: 1 + type: string + componentTools: + description: |- + componentTools references the ComponentTools CR (in the same namespace + as the Workflow) that provides skills, MCP servers, system prompt, and + output schema for this step. + properties: + name: + description: name of the ComponentTools. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + required: + - componentTools + type: object + required: + - analysis + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/config/crd/bases/ols.openshift.io_olsconfigs.yaml b/config/crd/bases/ols.openshift.io_olsconfigs.yaml index 869e1da55..51a061d8f 100644 --- a/config/crd/bases/ols.openshift.io_olsconfigs.yaml +++ b/config/crd/bases/ols.openshift.io_olsconfigs.yaml @@ -43,11 +43,12 @@ spec: featureGates: description: |- Feature Gates holds list of features to be enabled explicitly, otherwise they are disabled by default. - possible values: MCPServer, ToolFiltering + possible values: MCPServer, ToolFiltering, LightspeedAgents items: enum: - MCPServer - ToolFiltering + - LightspeedAgents type: string type: array llm: diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index f9198651e..b387cced3 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,6 +3,18 @@ # It should be run by config/default resources: - bases/ols.openshift.io_olsconfigs.yaml +- bases/agentic.openshift.io_agents.yaml +- bases/agentic.openshift.io_componenttools.yaml +- bases/agentic.openshift.io_llmproviders.yaml +- bases/agentic.openshift.io_approvalpolicies.yaml +- bases/agentic.openshift.io_proposalapprovals.yaml +- bases/agentic.openshift.io_proposals.yaml +- bases/agentic.openshift.io_analysisresults.yaml +- bases/agentic.openshift.io_escalationresults.yaml +- bases/agentic.openshift.io_executionresults.yaml +- bases/agentic.openshift.io_verificationresults.yaml +- bases/agentic.openshift.io_proposaltemplates.yaml +- bases/agentic.openshift.io_workflows.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/default/deployment-patch.yaml b/config/default/deployment-patch.yaml index bd465e805..e9611afac 100644 --- a/config/default/deployment-patch.yaml +++ b/config/default/deployment-patch.yaml @@ -24,6 +24,12 @@ - op: add path: /spec/template/spec/containers/0/args/- value: --ocp-rag-image=__REPLACE_LIGHTSPEED_OCP_RAG__ +- op: add + path: /spec/template/spec/containers/0/args/- + value: --agentic-console-image=__REPLACE_LIGHTSPEED_AGENTIC_CONSOLE__ +- op: add + path: /spec/template/spec/containers/0/args/- + value: --agentic-sandbox-image=__REPLACE_LIGHTSPEED_AGENTIC_SANDBOX__ - op: replace path: /spec/template/spec/containers/0/image value: __REPLACE_LIGHTSPEED_OPERATOR__ diff --git a/config/rbac-agentic/admin_role.yaml b/config/rbac-agentic/admin_role.yaml new file mode 100644 index 000000000..420acd73e --- /dev/null +++ b/config/rbac-agentic/admin_role.yaml @@ -0,0 +1,87 @@ +# lightspeed-agentic-admin grants cluster admins full management of the +# agentic platform's infrastructure CRDs (LLMProvider, Agent, ProposalTemplate) +# plus read and approve/deny access to Proposals. +# +# Per the actor model (see gist ac8e8399a9bf69091a38a5cf6e3bc56b): +# - Cluster Admin: installs the operator, configures LLM infrastructure, +# creates workflow templates, provides runtime credentials (Day 0). +# - Component Owner: only creates Proposals (Day N, use lightspeed-component-owner). +# - Operator: uses agentic-manager-role (separate ClusterRole). +# +# Bind to cluster admin users/groups via ClusterRoleBinding. +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: lightspeed-agentic-admin +rules: + # Full management of LLM infrastructure — provider configs, credentials, + # model selection. Only cluster admins should create/modify these. + - apiGroups: + - agentic.openshift.io + resources: + - llmproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + # Full management of agent tiers — LLM provider binding, timeouts, + # max turns, provider-specific settings (reasoningEffort, etc.). + - apiGroups: + - agentic.openshift.io + resources: + - agents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - agentic.openshift.io + resources: + - agents/status + verbs: + - get + # Full management of workflow templates — defines which steps run + # and which agent tier handles each step. + - apiGroups: + - agentic.openshift.io + resources: + - proposaltemplates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + # Read, approve/deny, and manage Proposals across all namespaces. + # Admins can also delete proposals for cleanup, but typically do + # not create them (that is the component owner's role). + - apiGroups: + - agentic.openshift.io + resources: + - proposals + verbs: + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - agentic.openshift.io + resources: + - proposals/status + verbs: + - get + - patch + - update diff --git a/config/rbac-agentic/component_owner_role.yaml b/config/rbac-agentic/component_owner_role.yaml new file mode 100644 index 000000000..92aca27fd --- /dev/null +++ b/config/rbac-agentic/component_owner_role.yaml @@ -0,0 +1,80 @@ +# lightspeed-component-owner grants product teams (ACS, CVO, CMO, etc.) +# the minimum permissions needed to integrate with the agentic platform. +# +# Per the actor model (see gist ac8e8399a9bf69091a38a5cf6e3bc56b): +# - Component Owner: ships skills images + adapter (webhook, event source) +# that creates Proposal CRs at runtime. They do NOT manage LLMProvider, +# Agent, or ProposalTemplate — those are cluster admin concerns. +# - Proposals can only reference Secrets in their own namespace (K8s RBAC +# enforces isolation without any custom logic). +# +# Bind with a RoleBinding (namespace-scoped) to restrict component teams +# to creating proposals in their own namespace only: +# +# kind: RoleBinding +# metadata: +# name: acs-lightspeed-component-owner +# namespace: stackrox +# roleRef: +# kind: ClusterRole +# name: lightspeed-component-owner +# subjects: +# - kind: ServiceAccount +# name: acs-violation-webhook +# namespace: stackrox +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: lightspeed-component-owner +rules: + # Create, read, and manage Proposals. Component teams create proposals + # via their adapter (webhook) and read status to track progress. + # Patch is needed for spec.revisionFeedback (iterative feedback). + # Delete allows cleanup of completed proposals. + - apiGroups: + - agentic.openshift.io + resources: + - proposals + verbs: + - create + - delete + - get + - list + - patch + - watch + # Read proposal status to track lifecycle progress. Patch is needed + # for the approve/deny flow (setting the Approved condition). + - apiGroups: + - agentic.openshift.io + resources: + - proposals/status + verbs: + - get + - patch + # Read-only access to ProposalTemplates — component teams reference + # templates by name (spec.templateRef) and need to discover which + # templates are available. + - apiGroups: + - agentic.openshift.io + resources: + - proposaltemplates + verbs: + - get + - list + - watch + # Read-only access to Agents — needed for inline proposals (no + # templateRef) where the component team specifies agent tiers + # directly (e.g., analysis.agent: smart). Also useful for + # understanding available tiers when authoring proposals. + - apiGroups: + - agentic.openshift.io + resources: + - agents + verbs: + - get + - list + - watch + # No access to LLMProviders — LLM infrastructure (credentials, + # models, endpoints) is a cluster admin concern. Component teams + # interact with agents by name, not with the underlying providers. diff --git a/config/rbac-agentic/kustomization.yaml b/config/rbac-agentic/kustomization.yaml new file mode 100644 index 000000000..909c546a7 --- /dev/null +++ b/config/rbac-agentic/kustomization.yaml @@ -0,0 +1,5 @@ +resources: +- role.yaml +- role_binding.yaml +- admin_role.yaml +- component_owner_role.yaml diff --git a/config/rbac-agentic/role.yaml b/config/rbac-agentic/role.yaml new file mode 100644 index 000000000..958cb0298 --- /dev/null +++ b/config/rbac-agentic/role.yaml @@ -0,0 +1,67 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: agentic-manager-role +rules: +- apiGroups: + - agentic.openshift.io + resources: + - agents + - approvalpolicies + - llmproviders + verbs: + - get + - list + - watch +- apiGroups: + - agentic.openshift.io + resources: + - analysisresults + - escalationresults + - executionresults + - verificationresults + verbs: + - create + - get + - list + - watch +- apiGroups: + - agentic.openshift.io + resources: + - proposalapprovals + - proposals + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - agentic.openshift.io + resources: + - proposalapprovals/status + - proposals/status + verbs: + - get + - patch + - update +- apiGroups: + - agentic.openshift.io + resources: + - proposals/finalizers + verbs: + - update +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get diff --git a/config/rbac-agentic/role_binding.yaml b/config/rbac-agentic/role_binding.yaml new file mode 100644 index 000000000..bb03f57fb --- /dev/null +++ b/config/rbac-agentic/role_binding.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: lightspeed-operator + app.kubernetes.io/part-of: lightspeed-operator + app.kubernetes.io/managed-by: kustomize + name: agentic-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: agentic-manager-role +subjects: +- kind: ServiceAccount + name: controller-manager + namespace: system diff --git a/go.mod b/go.mod index c2e692716..4f86a7276 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/openshift/lightspeed-operator -go 1.25.9 +go 1.25.7 require ( github.com/Jeffail/gabs/v2 v2.7.0 github.com/go-logr/logr v1.4.3 github.com/onsi/ginkgo/v2 v2.28.3 github.com/onsi/gomega v1.40.0 - github.com/openshift/client-go v0.0.0-20260428164731-4b85fc5b4e75 + github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a k8s.io/api v0.35.4 k8s.io/apimachinery v0.35.4 k8s.io/client-go v0.35.4 @@ -34,7 +34,7 @@ require ( github.com/containers/ocicrypt v1.3.0 // indirect github.com/containers/storage v1.59.1 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect - github.com/cyphar/filepath-securejoin v0.5.1 // indirect + github.com/cyphar/filepath-securejoin v0.5.2 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect @@ -42,7 +42,7 @@ require ( github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.10.0 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -112,8 +112,8 @@ require ( golang.org/x/sync v0.20.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/grpc v1.81.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect + google.golang.org/grpc v1.81.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect @@ -123,7 +123,9 @@ require ( require ( github.com/containers/image/v5 v5.36.2 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.91.0 + github.com/openshift/lightspeed-agentic-operator v0.0.0-20260515155741-a54ec75cdb3d + github.com/openshift/lightspeed-agentic-operator/api v0.0.0 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.90.1 ) require ( @@ -145,7 +147,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/openshift/api v0.0.0-20260420151639-34e60874783e + github.com/openshift/api v0.0.0-20260511191110-9b69e5fa27e9 github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -160,10 +162,15 @@ require ( golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/apiextensions-apiserver v0.35.4 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260427204847-8949caaa1199 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect ) + +// Upstream github.com/openshift/lightspeed-agentic-operator uses require api v0.0.0 with +// replace => ./api; replace directives are not applied to dependents, so pin the real +// submodule version here (same commit as the root module above). +replace github.com/openshift/lightspeed-agentic-operator/api v0.0.0 => github.com/openshift/lightspeed-agentic-operator/api v0.0.0-20260513171052-1682d6b70bae diff --git a/go.sum b/go.sum index be92073eb..40722f095 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,10 @@ github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHg github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48= -github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.5.2 h1:w/T2bhKr4pgwG0SUGjU4S/Is9+zUknLh5ROTJLzWX8E= +github.com/cyphar/filepath-securejoin v0.5.2/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= 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= @@ -87,8 +89,12 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.10.0 h1:Xx/5Ydg9CeBDX/wi4VJqStNtohYjitZhhlHt4h3St1M= -github.com/fsnotify/fsnotify v1.10.0/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= @@ -200,6 +206,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr 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.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -262,10 +270,18 @@ github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5 github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ= github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= -github.com/openshift/api v0.0.0-20260420151639-34e60874783e h1:ENxXUo0uksvseiBAoOcL9wdEWtueEpu84RE8Hm0q3uY= -github.com/openshift/api v0.0.0-20260420151639-34e60874783e/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= -github.com/openshift/client-go v0.0.0-20260428164731-4b85fc5b4e75 h1:UMBIwb0f9Zre46LksO8P7V8dCNrGOBUdn8fXDgAhepA= -github.com/openshift/client-go v0.0.0-20260428164731-4b85fc5b4e75/go.mod h1:lITKsplmZ9kJ6zvk4hW52XMZ9tt621GZGb69YSp+CSY= +github.com/openshift/api v0.0.0-20260424174501-4f63a40a2970 h1:xyz8VL2VnskV4YTDaHrAmBxFLyyPjxOt5dYZRBeAvmk= +github.com/openshift/api v0.0.0-20260424174501-4f63a40a2970/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/api v0.0.0-20260511191110-9b69e5fa27e9 h1:yb8ul1HPFYhO04yp0D8T/qSySZnKv210f4nE//i/Bdg= +github.com/openshift/api v0.0.0-20260511191110-9b69e5fa27e9/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/client-go v0.0.0-20260424153654-c280f7942f94 h1:EVKd4ZKP25wmPJnAj0dsz/7fGTHnNA8J0z4PXAq60lw= +github.com/openshift/client-go v0.0.0-20260424153654-c280f7942f94/go.mod h1:lITKsplmZ9kJ6zvk4hW52XMZ9tt621GZGb69YSp+CSY= +github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a h1:EKx2XhOKehd1C5ptY7IrLl4WV35E8kP0pRPnG5BUZXk= +github.com/openshift/client-go v0.0.0-20260512113608-deb4dc54551a/go.mod h1:V933kvY/cb/Un7UCEOhXHUySNX327u7Epe8g9KNqg2Q= +github.com/openshift/lightspeed-agentic-operator v0.0.0-20260515155741-a54ec75cdb3d h1:XPKZdjDdTUMYzoiuSoCLbaeyOpzVrLtxGEFaEw6QL3o= +github.com/openshift/lightspeed-agentic-operator v0.0.0-20260515155741-a54ec75cdb3d/go.mod h1:mVB1Tgp4cLKrpNuipvVtbMsuySv0n7SDft0LgmDWgqU= +github.com/openshift/lightspeed-agentic-operator/api v0.0.0-20260513171052-1682d6b70bae h1:VQ7k9ls4AmeYsFTor/m0ap4xbEOZEsiZGkqgIH/u4FY= +github.com/openshift/lightspeed-agentic-operator/api v0.0.0-20260513171052-1682d6b70bae/go.mod h1:tZlKXEZJ4/qxPPh6mamWrkwK+EQjudHt8AypudHttVs= 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= @@ -273,8 +289,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98= github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.91.0 h1:m2SZ2z5edgk0nXx7W6VHLfIsKZwgKbr+E5c2RNYyJB8= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.91.0/go.mod h1:Gfzi4500QCMnptFIQc8YdDi8YZ4QA0vs22LROWZ3+YU= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.90.1 h1:URbjn501/IBFTzPtGXrYDXHi+ZcbP2W60o6JeTrY3vQ= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.90.1/go.mod h1:Gfzi4500QCMnptFIQc8YdDi8YZ4QA0vs22LROWZ3+YU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -368,6 +384,8 @@ 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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= @@ -501,15 +519,15 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw= -google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -519,8 +537,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= diff --git a/hack/agentic/CLAUDE.md b/hack/agentic/CLAUDE.md new file mode 100644 index 000000000..60e8fdb15 --- /dev/null +++ b/hack/agentic/CLAUDE.md @@ -0,0 +1,74 @@ +# Agentic Deploy Scripts + +Scripts for building and deploying the agentic stack on OpenShift. All builds +run on-cluster via OpenShift BuildConfigs (binary source + Docker strategy) — +no local container engine needed. + +## Full deploy (fresh cluster) + +```bash +KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex +KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=bedrock +KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex --skip-build +KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex --with-demo +``` + +Deploys: CRDs, namespace, builds (agent + skills in parallel, then console, +then operator), LLMProvider, Agent tiers, ApprovalPolicy, SandboxTemplate. + +Required env vars for Vertex: `VERTEX_PROJECT`. For Bedrock: `AWS_ACCESS_KEY_ID`, +`AWS_SECRET_ACCESS_KEY` (or aws cli config). + +## Fast iteration (redeploy single component) + +```bash +KUBECONFIG=... bash hack/agentic/redeploy-operator.sh # operator only +KUBECONFIG=... bash hack/agentic/redeploy-agent.sh # agent sandbox + skills +KUBECONFIG=... bash hack/agentic/redeploy-console.sh # console plugin only +KUBECONFIG=... bash hack/agentic/redeploy-skills.sh # skills image only +KUBECONFIG=... bash hack/agentic/redeploy-all.sh # everything (parallel) +``` + +All scripts accept `--skip-build` to skip the image build and just rollout. + +## Teardown + +```bash +KUBECONFIG=... bash hack/agentic/undeploy.sh +KUBECONFIG=... VERTEX_PROJECT=... bash hack/agentic/undeploy.sh # also cleans GCP SA +``` + +## How builds work + +- `lib.sh` defines `build_on_cluster()` (sequential, streams logs) and + `start_build_async()` + `wait_all_builds()` (parallel, polls status). +- Each component has a BuildConfig + ImageStream in `openshift-lightspeed`. +- Images are tagged as `wt-` in worktrees, `latest` in main repo. + Multiple worktrees can deploy to the same cluster without clobbering. +- 4 images total: operator, agent sandbox, console plugin, skills. +- Skills is a single OCI image with all skills. Per-proposal skill selection + uses `SkillsSource.paths` in the Proposal CRD (no per-profile images needed). +- The operator build constructs a minimal context with just + `lightspeed-operator/` and `lightspeed-agentic-operator/` (copied to a temp + dir) — it does NOT upload the entire workspace root. + +## Repo path overrides + +All repo paths are auto-detected from the workspace layout but can be +overridden via environment variables: + +| Variable | Default | Used by | +|---|---|---| +| `AGENTIC_OPERATOR_DIR` | `../lightspeed-agentic-operator` | Operator build (Go types) | +| `AGENT_DIR` | `../lightspeed-agentic-sandbox` | Agent sandbox build | +| `CONSOLE_DIR` | `../lightspeed-agentic-console` | Console plugin build | +| `SKILLS_DIR` | `../agentic-skills` | Skills image build | + +## Components + +| Component | BuildConfig | Build context | Dockerfile | +|---|---|---|---| +| Operator | `lightspeed-operator` | `lightspeed-operator/` + `lightspeed-agentic-operator/` (minimal) | `lightspeed-operator/Dockerfile.dev` | +| Agent sandbox | `lightspeed-agentic-sandbox` | `lightspeed-agentic-sandbox/` | `Containerfile.dev` | +| Console plugin | `lightspeed-console-plugin` | `lightspeed-agentic-console/` | `Dockerfile` | +| Skills | `agentic-skills` | `agentic-skills/` | `Containerfile` | diff --git a/hack/agentic/deploy.sh b/hack/agentic/deploy.sh new file mode 100755 index 000000000..18d9ea41d --- /dev/null +++ b/hack/agentic/deploy.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# Deploy the full agentic stack on a fresh OpenShift cluster. +# For subsequent iterations, use the redeploy-*.sh scripts instead. +# +# Components deployed: +# - Operator (lightspeed-operator-controller-manager) +# - Agent sandbox (lightspeed-agent pod via SandboxTemplate) +# - Skills OCI image +# - Console plugin (lightspeed-agentic-console) +# - Proposal API chain (LLMProvider → Agent → ApprovalPolicy) +# +# Usage: +# KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex +# KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=bedrock +# KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex --skip-build +# KUBECONFIG=/path/to/kubeconfig bash hack/agentic/deploy.sh --provider=vertex --with-demo +# +# Environment variables: +# KUBECONFIG - Required. Path to cluster kubeconfig. +# LLM_PROVIDER - Alternative to --provider flag (vertex|bedrock). +# +# Vertex AI: +# VERTEX_PROJECT - Required. GCP project with Vertex AI enabled. +# VERTEX_REGION - GCP region (default: global). +# GOOGLE_APPLICATION_CREDENTIALS - Path to GCP credentials JSON (default: ~/.config/gcloud/application_default_credentials.json). +# +# AWS Bedrock: +# AWS_ACCESS_KEY_ID - Bedrock access key (or reads from aws cli config). +# AWS_SECRET_ACCESS_KEY - Bedrock secret key (or reads from aws cli config). +# AWS_REGION - Bedrock region (or reads from aws cli config). +# +# Optional secrets (reads from macOS Keychain if unset): +# GH_TOKEN - GitHub API token for agent tools. +# RH_API_OFFLINE_TOKEN - Red Hat API offline token for support tools. + +show_usage() { + echo "Usage: KUBECONFIG= bash hack/agentic/deploy.sh --provider= [--skip-build] [--with-demo]" + echo "" + echo "Flags:" + echo " --provider= LLM provider (required)" + echo " --skip-build Skip container image builds" + echo " --with-demo Deploy test fixtures (crash-looping demo app)" + echo "" + echo "See hack/agentic/CLAUDE.md for documentation." +} + +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib.sh" +parse_args "$@" + +if [[ -z "${LLM_PROVIDER}" ]]; then + fail "LLM provider not set. Use --provider=vertex or --provider=bedrock" +fi +case "${LLM_PROVIDER}" in + vertex|bedrock) ;; + *) fail "Unknown provider: ${LLM_PROVIDER}. Supported: vertex, bedrock" ;; +esac + +############################################################################### +# Day 0 — Operator Installation (Cluster Admin) +# Timeline ref: gist harche/ac8e8399a9bf69091a38a5cf6e3bc56b +############################################################################### +check_cluster +install_crds +ensure_namespace +ensure_buildconfigs + +# Build agent + skills in parallel (agent is heavy, skills is instant). +# Console runs after to avoid memory pressure from concurrent heavy builds. +# Operator must wait for manifests (which reference the console image). +step "Building images (agent + skills parallel, then console)" +[[ -d "${AGENT_DIR}" ]] && start_build_async "${BC_AGENT}" "${AGENT_DIR}" "agent sandbox" +[[ -d "${SKILLS_DIR}" ]] && start_build_async "${BC_SKILLS}" "${SKILLS_DIR}" "skills" +wait_all_builds + +[[ -d "${CONSOLE_DIR}" ]] && build_on_cluster "${BC_CONSOLE}" "${CONSOLE_DIR}" "console plugin" + +if [[ -d "${CONSOLE_DIR}" ]]; then + oc policy add-role-to-user system:image-puller \ + system:serviceaccount:${NS_CONSOLE}:lightspeed-agentic-console-plugin \ + -n "${NS_CONSOLE}" >/dev/null 2>&1 + info "Console SA granted image-puller" +fi + +deploy_operator_manifests +ensure_agentic_feature_gate +build_push_operator +install_agent_sandbox_controller +ensure_agent_rbac +ensure_agent_service + +############################################################################### +# Day 0, Step 1 — LLM credentials + LLMProvider CRs (Cluster Admin) +############################################################################### +LLM_SECRET="llm-credentials" + +if [[ "${LLM_PROVIDER}" == "vertex" ]]; then + step "Ensuring LLM credentials (Vertex AI)" + VERTEX_REGION="${VERTEX_REGION:-global}" + + if ! oc get secret "${LLM_SECRET}" -n "${NS_OPERATOR}" >/dev/null 2>&1; then + GCP_CREDS_FILE=$(vertex_credentials_file) + + oc create secret generic "${LLM_SECRET}" -n "${NS_OPERATOR}" \ + --from-file=credentials.json="${GCP_CREDS_FILE}" \ + --from-literal=ANTHROPIC_VERTEX_PROJECT_ID="${VERTEX_PROJECT}" \ + --from-literal=CLOUD_ML_REGION="${VERTEX_REGION}" >/dev/null 2>&1 + info "LLM credentials created (ADC, project=${VERTEX_PROJECT}, region=${VERTEX_REGION})" + else + info "LLM credentials already exist" + fi + +elif [[ "${LLM_PROVIDER}" == "bedrock" ]]; then + step "Ensuring LLM credentials (Bedrock)" + BEDROCK_ACCESS_KEY="${AWS_ACCESS_KEY_ID:-$(aws configure get aws_access_key_id 2>/dev/null || true)}" + BEDROCK_SECRET_KEY="${AWS_SECRET_ACCESS_KEY:-$(aws configure get aws_secret_access_key 2>/dev/null || true)}" + BEDROCK_REGION="${AWS_REGION:-$(aws configure get region 2>/dev/null || echo us-east-1)}" + + if ! oc get secret "${LLM_SECRET}" -n "${NS_OPERATOR}" >/dev/null 2>&1; then + if [[ -n "${BEDROCK_ACCESS_KEY}" ]] && [[ -n "${BEDROCK_SECRET_KEY}" ]]; then + oc create secret generic "${LLM_SECRET}" -n "${NS_OPERATOR}" \ + --from-literal=AWS_ACCESS_KEY_ID="${BEDROCK_ACCESS_KEY}" \ + --from-literal=AWS_SECRET_ACCESS_KEY="${BEDROCK_SECRET_KEY}" \ + --from-literal=AWS_REGION="${BEDROCK_REGION}" >/dev/null 2>&1 + info "LLM credentials created (Bedrock: region=${BEDROCK_REGION})" + else + fail "AWS credentials not found — set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or configure aws cli" + fi + else + info "LLM credentials already exist" + fi +fi + +############################################################################### +# Day 0, Step 4 — Runtime secrets in operator namespace (Cluster Admin) +# Tool credentials (GitHub, Red Hat API, ACS) for agent sandbox pods. +############################################################################### +ensure_tool_secrets + +############################################################################### +# Base SandboxTemplate — provider-agnostic. The operator patches in LLM +# credentials, skills, MCP servers, and phase config from the CRD chain +# (Agent + ComponentTools + LLMProvider) at proposal reconciliation time. +############################################################################### +AGENT_IMAGE="${INTERNAL_REG}/${NS_OPERATOR}/lightspeed-agentic-sandbox:${TAG}" + +step "Deploying base SandboxTemplate" +cat </dev/null 2>&1 +apiVersion: extensions.agents.x-k8s.io/v1alpha1 +kind: SandboxTemplate +metadata: + name: lightspeed-agent + namespace: openshift-lightspeed +spec: + networkPolicyManagement: Unmanaged + podTemplate: + spec: + serviceAccountName: lightspeed-agent + automountServiceAccountToken: true + containers: + - name: agent + image: ${AGENT_IMAGE} + imagePullPolicy: Always + ports: + - containerPort: 8080 + protocol: TCP + env: + - name: LIGHTSPEED_SKILLS_DIR + value: /app/skills + volumeMounts: + - name: skills + mountPath: /app/skills + - name: home + mountPath: /home/agent + - name: tmp + mountPath: /tmp + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: "4" + memory: 4Gi + volumes: + - name: skills + image: + reference: placeholder:latest + pullPolicy: Always + - name: home + emptyDir: {} + - name: tmp + emptyDir: {} +SANDBOXEOF +info "Base SandboxTemplate created" + +############################################################################### +# Day 0, Steps 1-3 — Proposal API chain (Cluster Admin) +# LLMProvider → Agent → ApprovalPolicy +# See timeline: gist harche/ac8e8399a9bf69091a38a5cf6e3bc56b +############################################################################### +step "Setting up proposal API chain (Day 0)" + +if oc get secret "${LLM_SECRET}" -n "${NS_OPERATOR}" >/dev/null 2>&1; then + if [[ "${LLM_PROVIDER}" == "vertex" ]]; then + cat </dev/null 2>&1 +apiVersion: agentic.openshift.io/v1alpha1 +kind: LLMProvider +metadata: + name: vertex-ai +spec: + type: GoogleCloudVertex + googleCloudVertex: + credentialsSecret: + name: ${LLM_SECRET} + projectID: ${VERTEX_PROJECT} + region: ${VERTEX_REGION} +LLMEOF + info "LLMProvider CR created (vertex-ai via GoogleCloudVertex)" + + elif [[ "${LLM_PROVIDER}" == "bedrock" ]]; then + cat </dev/null 2>&1 +apiVersion: agentic.openshift.io/v1alpha1 +kind: LLMProvider +metadata: + name: bedrock +spec: + type: AWSBedrock + awsBedrock: + credentialsSecret: + name: ${LLM_SECRET} + region: ${BEDROCK_REGION} +LLMEOF + info "LLMProvider CR created (bedrock via AWSBedrock)" + fi +fi + +setup_proposal_agents_and_workflows + +if [[ "${WITH_DEMO}" == "true" ]]; then + deploy_test_fixtures +fi + +verify_deploy + +echo -e "\n${GREEN}Full agentic stack deployed (provider: ${LLM_PROVIDER}).${NC}" +echo -e " Day 0 complete: LLMProvider → Agent → ApprovalPolicy" +if [[ "${WITH_DEMO}" == "true" ]]; then + echo -e " Demo: JVM OOMKill pod deployed in lightspeed-demo, AlertManager proposal submitted" +else + echo -e " Day 1 (create a proposal): oc apply -f ../lightspeed-agentic-operator/examples/setup/03-proposals.yaml" + echo -e " Or deploy JVM OOMKill demo: re-run with --with-demo" +fi diff --git a/hack/agentic/lib.sh b/hack/agentic/lib.sh new file mode 100755 index 000000000..833e5f856 --- /dev/null +++ b/hack/agentic/lib.sh @@ -0,0 +1,951 @@ +#!/usr/bin/env bash +# Shared helpers for agentic deploy/redeploy scripts. +# Source this file; do not execute directly. + +set -euo pipefail + +RED='\033[0;31m' GREEN='\033[0;32m' CYAN='\033[0;36m' YELLOW='\033[0;33m' NC='\033[0m' +step() { echo -e "\n${CYAN}==> $1${NC}"; } +info() { echo -e " ${GREEN}✓${NC} $1"; } +warn() { echo -e " ${YELLOW}!${NC} $1"; } +fail() { echo -e " ${RED}✗${NC} $1"; exit 1; } + +_run() { + local _out + _out=$(mktemp) + if "$@" >"${_out}" 2>&1; then + rm -f "${_out}" + else + local _rc=$? + echo -e " ${RED}✗${NC} Command failed: $*" >&2 + cat "${_out}" >&2 + rm -f "${_out}" + return ${_rc} + fi +} + +# Paths — this file lives in lightspeed-operator/hack/agentic/ +# Sibling repos are next to the operator repo in the workspace. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[1]}")" && pwd)" +OPERATOR_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +WORKSPACE_ROOT="$(dirname "${OPERATOR_DIR}")" + +CONSOLE_DIR="${CONSOLE_DIR:-${WORKSPACE_ROOT}/lightspeed-agentic-console}" +SKILLS_DIR="${SKILLS_DIR:-${WORKSPACE_ROOT}/agentic-skills}" +AGENT_DIR="${AGENT_DIR:-${WORKSPACE_ROOT}/lightspeed-agentic-sandbox}" +AGENTIC_OPERATOR_DIR="${AGENTIC_OPERATOR_DIR:-${WORKSPACE_ROOT}/lightspeed-agentic-operator}" + +# Namespaces +NS_OPERATOR="openshift-lightspeed" +NS_CONSOLE="openshift-lightspeed" + +# Deployment names (match operator constants.go) +DEPLOY_OPERATOR="lightspeed-operator-controller-manager" +DEPLOY_CONSOLE="lightspeed-agentic-console-plugin" + +# Image tag — unique per worktree so concurrent deploys don't clobber each other. +# .worktrees// → "wt-", main repo → "latest". +if [[ "${WORKSPACE_ROOT}" == */.worktrees/* ]]; then + TAG="wt-$(basename "${WORKSPACE_ROOT}")" +else + TAG="latest" +fi + +# BuildConfig names — match ImageStream names in ensure_buildconfigs() +BC_OPERATOR="lightspeed-operator" +BC_CONSOLE="lightspeed-console-plugin" +BC_AGENT="lightspeed-agentic-sandbox" +BC_SKILLS="agentic-skills" + +# Internal registry endpoint (for image references inside the cluster) +INTERNAL_REG="image-registry.openshift-image-registry.svc:5000" + +# Centralized image references — used by deploy_operator_manifests and redeploy scripts +OPERATOR_IMG="${INTERNAL_REG}/${NS_OPERATOR}/${BC_OPERATOR}:${TAG}" +CONSOLE_IMG="${INTERNAL_REG}/${NS_OPERATOR}/${BC_CONSOLE}:${TAG}" +AGENT_IMG="${INTERNAL_REG}/${NS_OPERATOR}/${BC_AGENT}:${TAG}" +SKILLS_IMG="${INTERNAL_REG}/${NS_OPERATOR}/${BC_SKILLS}:${TAG}" + +show_usage() { echo "Usage: KUBECONFIG= bash $0 [--skip-build]"; } + +# Parse flags: --skip-build, --provider=, --with-demo +SKIP_BUILD=false +LLM_PROVIDER="" +WITH_DEMO=false +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-build) SKIP_BUILD=true; shift ;; + --provider=*) LLM_PROVIDER="${1#*=}"; shift ;; + --provider) LLM_PROVIDER="${2:-}"; shift 2 ;; + --with-demo) WITH_DEMO=true; shift ;; + --help|-h) show_usage; exit 0 ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac + done +} + +# Verify cluster access +check_cluster() { + [[ -z "${KUBECONFIG:-}" ]] && fail "KUBECONFIG not set" + oc whoami >/dev/null 2>&1 || fail "Cannot reach cluster (check KUBECONFIG)" + info "Cluster: $(oc whoami --show-server 2>/dev/null | sed 's|https://||')" +} + +# Ensure ImageStreams and BuildConfigs exist for all components. +# BuildConfigs use Binary source + Docker strategy — builds run on the +# cluster natively (no local container engine or cross-compilation needed). +# Idempotent (oc apply), safe to call from every script. +ensure_buildconfigs() { + step "Ensuring BuildConfigs and ImageStreams" + cat <