From d37f7ad56c9f80bc8e74406b6aea4db447df771e 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] 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 + api/v1alpha1/olsconfig_types.go | 4 +- cmd/main.go | 28 + .../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 | 59 +- go.sum | 98 +- 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/controller/utils/constants.go | 7 + related_images.json | 40 +- 37 files changed, 7441 insertions(+), 89 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 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/api/v1alpha1/olsconfig_types.go b/api/v1alpha1/olsconfig_types.go index 55963b5a9..12e1747f4 100644 --- a/api/v1alpha1/olsconfig_types.go +++ b/api/v1alpha1/olsconfig_types.go @@ -41,13 +41,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 0aa887044..2347c652d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -81,6 +81,9 @@ import ( olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" + agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1" + agenticcontroller "github.com/openshift/lightspeed-agentic-operator/controller" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/openshift/lightspeed-operator/internal/controller" @@ -115,6 +118,7 @@ func init() { utilruntime.Must(configv1.AddToScheme(scheme)) utilruntime.Must(olsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(agenticv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -184,6 +188,8 @@ func main() { var lcoreImage 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.") @@ -206,6 +212,8 @@ func main() { flag.StringVar(&lcoreImage, "lcore-image", utils.LlamaStackImageDefault, "The image of the LCore 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{ @@ -461,6 +469,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 = agenticcontroller.Setup(mgr, agenticcontroller.Options{ + Namespace: namespace, + AgenticConsoleImage: agenticConsoleImage, + AgenticSandboxImage: 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 55bb40f04..afecad651 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 6ed99767d..0f6f4e67f 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/onsi/ginkgo/v2 v2.28.2 + github.com/onsi/gomega v1.39.1 + github.com/openshift/client-go v0.0.0-20260424153654-c280f7942f94 k8s.io/api v0.35.4 k8s.io/apimachinery v0.35.4 k8s.io/client-go v0.35.4 @@ -16,10 +16,11 @@ require ( ) require ( + cyphar.com/go-pathrs v0.2.1 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect - github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.14.1 // indirect github.com/VividCortex/ewma v1.2.0 // indirect @@ -34,7 +35,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.6.1 // 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,8 +43,8 @@ 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/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/swag/cmdutils v0.26.0 // indirect @@ -65,7 +66,7 @@ require ( github.com/google/go-intervals v0.0.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect - github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mattn/go-sqlite3 v1.14.39 // indirect @@ -82,7 +83,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect - github.com/opencontainers/selinux v1.14.1 // indirect + github.com/opencontainers/selinux v1.13.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/proglottis/gpgme v0.1.6 // indirect @@ -107,23 +108,27 @@ require ( go.opentelemetry.io/otel/trace v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/mod v0.36.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.35.0 // indirect 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/grpc v1.80.0 // 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 + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect + open-cluster-management.io/api v1.3.0 // indirect + open-cluster-management.io/managed-serviceaccount v0.10.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect ) 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-00010101000000-000000000000 + github.com/openshift/lightspeed-agentic-operator/api v0.0.0 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.90.1 ) require ( @@ -139,31 +144,37 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260507013755-92041b743c96 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect 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-20260424174501-4f63a40a2970 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 github.com/prometheus/procfs v0.20.1 // indirect github.com/spf13/pflag v1.0.10 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.28.0 // indirect - golang.org/x/net v0.54.0 // indirect + go.uber.org/zap v1.27.1 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.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 + golang.org/x/tools v0.44.0 // 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 ) + +replace github.com/openshift/lightspeed-agentic-operator => ../lightspeed-agentic-operator + +replace github.com/openshift/lightspeed-agentic-operator/api => ../lightspeed-agentic-operator/api + +replace github.com/harche/lightspeed-agentic-operator => ../lightspeed-agentic-operator diff --git a/go.sum b/go.sum index 9cc538ad8..03984f881 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= +cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -8,8 +10,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= -github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= -github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= @@ -53,8 +55,8 @@ 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.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,10 +89,10 @@ 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/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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -182,8 +184,8 @@ github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCj github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260507013755-92041b743c96 h1:YDDnaZ9afWajDboPMt9Vikqca/yWAX7KAxVzb4lJU1M= -github.com/google/pprof v0.0.0-20260507013755-92041b743c96/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -200,8 +202,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.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +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/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -250,22 +252,22 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.28.3 h1:4JvMdwtFU0imd8fHx25OJXoDMRexnf8v5NHKYSTTji4= -github.com/onsi/ginkgo/v2 v2.28.3/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= -github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= +github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= 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/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= +github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= +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/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/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 +275,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,8 +370,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.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= -go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -382,8 +384,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -395,8 +397,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -413,8 +415,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -446,8 +448,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -457,8 +459,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -468,8 +470,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -484,8 +486,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -508,8 +510,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac 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.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= 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 +521,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= @@ -547,8 +549,12 @@ k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260427204847-8949caaa1199 h1:sWu4Td5mgJlwunsUydnhKEAfNUHM7hm1wfKEQmD7G5c= k8s.io/kube-openapi v0.0.0-20260427204847-8949caaa1199/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc= -k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +open-cluster-management.io/api v1.3.0 h1:Q3miH38BE3N5+PesHQ0kcFi5nhX5350m7OJWapZcVqY= +open-cluster-management.io/api v1.3.0/go.mod h1:t0DsBv4gjIo9ojd7GYfA2tcEMpNf0h5Ix68pFDXwNSk= +open-cluster-management.io/managed-serviceaccount v0.10.0 h1:OBJ1EA8aaf0q8a7tBIHZT+5KdFECRKiDvoq/jqSHf1k= +open-cluster-management.io/managed-serviceaccount v0.10.0/go.mod h1:VbPxD7JBoGHVdZIw+jHmNKuY5dVOOvKWIIREM/WO2jU= sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 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 <