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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .ai/spec/what/audit-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ Implementation spec for compliance audit logging in the agentic operator. Parent
| `audit.escalation.completed` | EscalationResult CR created | EscalationResult serialization |
| `audit.proposal.terminal` | Proposal reaches terminal phase (Completed, Failed, Denied, Escalated) | Final phase, terminal reason |

2. CR serialization MUST include `.spec` plus `metadata.name`, `metadata.namespace`, `metadata.creationTimestamp`, and `metadata.uid`. Not the full Kubernetes metadata.
2. CR serialization MUST include `.spec` plus `metadata.name`, `metadata.namespace`, `metadata.creationTimestamp`, and `metadata.uid`. Not the full Kubernetes metadata. Result CRs (AnalysisResult, ExecutionResult, VerificationResult, EscalationResult) MUST also include `.status` since the useful data (RemediationOptions, ActionsTaken, Checks, etc.) lives in status.

3. Audit events MUST be emitted from the reconciliation loop where the operator already has the Proposal object in scope. The `trace_id` is read from the Proposal's `metadata.uid`.
3. All reconcile-emitted audit events MUST be emitted from the reconciliation loop where the operator already has the Proposal object in scope. The `trace_id` is read from the Proposal's `metadata.uid`. (`audit.approval.received` is webhook-emitted as defined below.)

### OTEL Spans

4. The operator MUST create a root span `proposal.lifecycle` when it first detects a new Proposal CR. The OTEL trace ID MUST be the Proposal's `metadata.uid` with hyphens stripped.
4. The operator MUST create a root span `proposal.received` when it first detects a new Proposal CR. The OTEL trace ID MUST be the Proposal's `metadata.uid` with hyphens stripped.

5. On operator restart, the operator MUST read the Proposal's `metadata.uid` from the CR and resume the trace by constructing a SpanContext with the same trace ID.

6. Child spans MUST be created for each phase: `proposal.analyze`, `proposal.human_approval`, `proposal.execute`, `proposal.verify`, `proposal.escalate`.
6. Child spans MUST be created for each phase: `proposal.analyze`, `proposal.human_approval`, `proposal.execute`, `proposal.verify`, `proposal.escalate`, `proposal.terminal`.

7. `proposal.human_approval` starts when the operator begins waiting for approval and ends when the ProposalApproval PATCH is observed. Duration = human decision time.

Expand Down Expand Up @@ -60,11 +60,11 @@ Implementation spec for compliance audit logging in the agentic operator. Parent

### Configuration

16. The operator reads audit config from the `AgenticOLSConfig` CR at `spec.audit`. If `spec.audit` is absent, the default is `enabled: true` with no OTEL export.
16. The operator reads audit config from the `AgenticOLSConfig` CR at `spec.audit`. Logging and tracing are independent controls.

17. When `spec.audit.enabled` is `true` (or absent — default), all audit events emit. When explicitly `false`, no audit events emit.
17. `spec.audit.logging` controls structured JSON audit events to stdout. Defaults to `true` — when the CR is absent or the field is not set, audit logging is enabled. Set to `false` to disable structured audit log output.

18. When `spec.audit.otel.endpoint` is set, the operator configures an OTLP exporter pointed at that endpoint. When empty or absent, a no-op exporter is used.
18. `spec.audit.otel.endpoint` controls OTEL trace export. When set, the operator configures an OTLP exporter pointed at that endpoint. When empty or absent, a no-op exporter is used. Independent of the `logging` flag — tracing works regardless of whether logging is on or off.

19. The operator MUST pass the OTEL endpoint to the sandbox via environment variable or config mount so the sandbox can configure its own exporter.

Expand Down
68 changes: 68 additions & 0 deletions api/v1alpha1/agenticolsconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,65 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AuditOTELConfig holds the optional OTEL exporter endpoint for audit tracing.
//
// +kubebuilder:validation:MinProperties=1
type AuditOTELConfig struct {
// endpoint is the OTLP gRPC endpoint URL (e.g. "jaeger-otlp-grpc.observability.svc:4317").
// When empty or absent, a no-op exporter is used — no traces are sent.
// +optional
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
Endpoint string `json:"endpoint,omitempty"`

// insecure disables TLS for the OTLP gRPC connection. Defaults to true
// for backward compatibility with in-cluster collectors. Set to false
// to require TLS when connecting to external or production endpoints.
// +optional
Insecure *bool `json:"insecure,omitempty"` //nolint:kubeapilinter // on/off toggle is genuinely binary
}

// AuditConfig controls compliance audit logging and tracing independently.
//
// +kubebuilder:validation:MinProperties=1
type AuditConfig struct {
// logging controls whether structured JSON audit events are emitted to stdout.
// Defaults to true. When the CR is absent or this field is not set, audit
// logging is enabled. Set to false to disable structured audit log output.
// +optional
Logging *bool `json:"logging,omitempty"` //nolint:kubeapilinter // on/off toggle is genuinely binary

// otel holds the optional OTEL exporter configuration for distributed tracing.
// Independent of the logging flag — tracing is enabled when an endpoint is set,
// regardless of whether logging is on or off.
// +optional
OTEL *AuditOTELConfig `json:"otel,omitzero"` //nolint:kubeapilinter // pointer needed for nil vs zero distinction
}

// LoggingEnabled returns true if structured audit logging is enabled (default: true when nil).
func (c *AuditConfig) LoggingEnabled() bool {
if c == nil || c.Logging == nil {
return true
}
return *c.Logging
}

// OTELEndpoint returns the configured OTEL endpoint, or empty string if not set.
func (c *AuditConfig) OTELEndpoint() string {
if c == nil || c.OTEL == nil {
return ""
}
return c.OTEL.Endpoint
}

// OTELInsecure returns whether the OTLP connection should use plaintext (default: true).
func (c *AuditConfig) OTELInsecure() bool {
if c == nil || c.OTEL == nil || c.OTEL.Insecure == nil {
return true
}
return *c.OTEL.Insecure
}

// AgenticOLSConfigSpec defines the desired state of AgenticOLSConfig.
//
// +kubebuilder:validation:MinProperties=1
Expand All @@ -32,6 +91,11 @@ type AgenticOLSConfigSpec struct {
// +optional
// +default=false
Suspended bool `json:"suspended,omitempty"` //nolint:kubeapilinter // kill switch is genuinely binary; bool is the right type

// audit controls compliance audit logging. When absent, audit logging
// is enabled by default with no OTEL export (structured JSON to stdout only).
// +optional
Audit *AuditConfig `json:"audit,omitzero"` //nolint:kubeapilinter // pointer needed for nil vs zero distinction in AuditEnabled()
}

// +kubebuilder:object:root=true
Expand All @@ -56,6 +120,10 @@ type AgenticOLSConfigSpec struct {
// name: cluster
// spec:
// suspended: false
// audit:
// logging: true
// otel:
// endpoint: "jaeger-otlp-grpc.observability.svc:4317"
type AgenticOLSConfig struct {
metav1.TypeMeta `json:",inline"`

Expand Down
90 changes: 89 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
package main

import (
"context"
"flag"
"os"

// Import auth plugins (Azure, GCP, OIDC, etc.) for local and hosted kubeconfigs.
_ "k8s.io/client-go/plugin/pkg/client/auth"

"github.com/go-logr/logr"
consolev1 "github.com/openshift/api/console/v1"
openshiftv1 "github.com/openshift/api/operator/v1"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1"
agenticcontroller "github.com/openshift/lightspeed-agentic-operator/controller"
"github.com/openshift/lightspeed-agentic-operator/controller/proposal"
)

var scheme = runtime.NewScheme()
Expand Down Expand Up @@ -87,12 +97,48 @@ func main() {
os.Exit(1)
}

// Read audit config from AgenticOLSConfig (best-effort at startup).
// spec.audit.logging and spec.audit.otel are independent controls.
auditLogging := true
var otelEndpoint string
otelInsecure := true
directClient, err := client.New(cfg, client.Options{Scheme: scheme})
if err != nil {
log.Error(err, "unable to create direct client for audit config")
os.Exit(1)
}
var agenticConfig agenticv1alpha1.AgenticOLSConfig
if err := directClient.Get(context.Background(), client.ObjectKey{Name: "cluster"}, &agenticConfig); err == nil {
auditLogging = proposal.ResolveLoggingEnabled(agenticConfig.Spec.Audit)
otelEndpoint = proposal.ResolveOTELEndpoint(agenticConfig.Spec.Audit)
otelInsecure = proposal.ResolveOTELInsecure(agenticConfig.Spec.Audit)
}
Comment on lines +111 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle audit config read failures explicitly.

Line 111 silently falls back for every Get error, so RBAC/API failures can disable configured OTEL tracing or apply the wrong audit logging mode without any signal. Default only for NotFound; log/fail closed for other errors, and bound the API call with a timeout.

Suggested fix
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
 	var agenticConfig agenticv1alpha1.AgenticOLSConfig
-	if err := directClient.Get(context.Background(), client.ObjectKey{Name: "cluster"}, &agenticConfig); err == nil {
+	if err := directClient.Get(ctx, client.ObjectKey{Name: "cluster"}, &agenticConfig); err != nil {
+		if !apierrors.IsNotFound(err) {
+			log.Error(err, "unable to read AgenticOLSConfig audit config")
+			os.Exit(1)
+		}
+	} else {
 		auditLogging = proposal.ResolveLoggingEnabled(agenticConfig.Spec.Audit)
 		otelEndpoint = proposal.ResolveOTELEndpoint(agenticConfig.Spec.Audit)
 		otelInsecure = proposal.ResolveOTELInsecure(agenticConfig.Spec.Audit)
 	}

As per coding guidelines, Go security requires “Never ignore error returns” and “context.Context for cancellation and timeouts”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 111 - 115, The directClient.Get() call silently
falls back on all errors, allowing RBAC and API failures to disable OTEL tracing
or audit logging without any indication. Replace context.Background() with a
context that has a timeout deadline, then explicitly check the error returned
from directClient.Get() to only silently continue for NotFound errors. For other
error types (such as permission denied or connection failures), log the error
appropriately and either fail closed or apply conservative defaults to ensure
security configurations are not accidentally disabled by API failures.

Source: Coding guidelines


// Create audit logger when logging is enabled (nil when disabled).
var auditLogger *proposal.AuditLogger
if auditLogging {
auditLogger = proposal.NewAuditLogger()
defer func() {
// Sync stdout returns "invalid argument" on many platforms — benign.
_ = auditLogger.Sync()
}()
Comment on lines +121 to +124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don’t suppress every audit logger sync error.

Line 123 hides real flush failures along with the known stdout EINVAL case. Handle or explicitly filter the benign error instead of discarding all errors.

Suggested fix
 	defer func() {
 		// Sync stdout returns "invalid argument" on many platforms — benign.
-		_ = auditLogger.Sync()
+		if err := auditLogger.Sync(); err != nil && !errors.Is(err, syscall.EINVAL) {
+			log.Error(err, "failed to sync audit logger")
+		}
 	}()

As per coding guidelines, Go security requires “Never ignore error returns”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defer func() {
// Sync stdout returns "invalid argument" on many platforms — benign.
_ = auditLogger.Sync()
}()
defer func() {
// Sync stdout returns "invalid argument" on many platforms — benign.
if err := auditLogger.Sync(); err != nil && !errors.Is(err, syscall.EINVAL) {
log.Error(err, "failed to sync audit logger")
}
}()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 121 - 124, The defer function containing
auditLogger.Sync() is currently suppressing all errors with the blank
identifier, which masks real flush failures. Instead of discarding all errors
indiscriminately, capture the error returned from auditLogger.Sync() and
explicitly check if it is the known benign "invalid argument" (EINVAL) error
that occurs on many platforms when syncing stdout. Only suppress this specific
error while handling or logging any other unexpected errors that might indicate
real problems during flush operations.

Source: Coding guidelines

}

// Initialize OTEL TracerProvider — no-op when endpoint is empty.
tp := initTracerProvider(otelEndpoint, otelInsecure, log)
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
Comment on lines +129 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect OTEL init/shutdown call sites and any documented shutdown grace period.
rg -n -C3 'initTracerProvider|Shutdown\(context\.Background\(\)|otlptracegrpc\.New|terminationGracePeriodSeconds' cmd/main.go config/ .ai/spec/ README.md

Repository: openshift/lightspeed-agentic-operator

Length of output: 1158


🏁 Script executed:

#!/bin/bash
# Read the full initTracerProvider function to see ctx definition and usage
sed -n '163,203p' cmd/main.go

Repository: openshift/lightspeed-agentic-operator

Length of output: 1325


🏁 Script executed:

#!/bin/bash
# Check for timeout patterns in main.go and deployment config
echo "=== Timeout patterns in main.go ==="
grep -n "WithTimeout\|time.Second\|context.WithDeadline" cmd/main.go

echo -e "\n=== Deployment terminationGracePeriodSeconds ==="
find config/ -name "*.yaml" -o -name "*.yml" | xargs grep -l "terminationGracePeriodSeconds" 2>/dev/null || echo "Not found in config/"

echo -e "\n=== Manager deployment configuration ==="
cat config/manager/manager.yaml 2>/dev/null | head -80

Repository: openshift/lightspeed-agentic-operator

Length of output: 2181


🏁 Script executed:

#!/bin/bash
# Check go.mod for OTEL dependencies
echo "=== OTEL dependencies in go.mod ==="
grep -i "otel\|trace" go.mod

echo -e "\n=== Check for OTEL SDK timeout behavior in imports ==="
grep -n "otlptracegrpc\|sdktrace\|context" cmd/main.go | head -20

Repository: openshift/lightspeed-agentic-operator

Length of output: 1319


Add timeout contexts for OTEL exporter creation and shutdown.

Exporter creation at line 180 and shutdown at line 130 use context.Background() without timeouts. If the OTEL collector is unreachable or slow, these operations can hang indefinitely and delay operator startup or graceful termination. Go security guidelines require "context.Context for cancellation and timeouts."

Suggested fix
 	defer func() {
-		if err := tp.Shutdown(context.Background()); err != nil {
+		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+		if err := tp.Shutdown(shutdownCtx); err != nil {
 			log.Error(err, "failed to shut down tracer provider")
 		}
 	}()
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
 	opts := []otlptracegrpc.Option{
 		otlptracegrpc.WithEndpoint(endpoint),
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 129 - 130, The tp.Shutdown() call at line 130 and
the OTEL exporter creation at line 180 both use context.Background() without
timeouts, which can cause indefinite hangs if the OTEL collector is unreachable.
Replace context.Background() with context.WithTimeout() at both locations to add
appropriate timeout durations (e.g., 10-30 seconds), ensuring that exporter
creation and shutdown operations fail fast rather than blocking indefinitely
during operator startup or graceful termination.

Source: Coding guidelines

log.Error(err, "failed to shut down tracer provider")
}
}()

if err := agenticcontroller.Setup(mgr, agenticcontroller.Options{
Namespace: namespace,
AgenticConsoleImage: agenticConsoleImage,
AgenticSandboxImage: agenticSandboxImage,
SandboxMode: sandboxMode,
ImagePullPolicy: imagePullPolicy,
Audit: auditLogger,
}); err != nil {
log.Error(err, "unable to set up agentic controllers")
os.Exit(1)
Expand All @@ -107,9 +153,51 @@ func main() {
os.Exit(1)
}

log.Info("starting manager", "namespace", namespace)
log.Info("starting manager", "namespace", namespace, "auditLogging", auditLogging, "otelEndpoint", otelEndpoint)
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
log.Error(err, "problem running manager")
os.Exit(1)
Comment on lines 157 to 159

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flush telemetry before exiting on manager errors.

os.Exit(1) skips the deferred tp.Shutdown and auditLogger.Sync above, so batched spans can be lost on the failure path. Explicitly run cleanup before exiting, or move main logic into a run() int helper so defers execute before os.Exit.

Suggested pattern
-	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
-		log.Error(err, "problem running manager")
-		os.Exit(1)
-	}
+	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+		log.Error(err, "problem running manager")
+		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		if shutdownErr := tp.Shutdown(shutdownCtx); shutdownErr != nil {
+			log.Error(shutdownErr, "failed to shut down tracer provider")
+		}
+		cancel()
+		if auditLogger != nil {
+			if syncErr := auditLogger.Sync(); syncErr != nil {
+				log.Error(syncErr, "failed to sync audit logger")
+			}
+		}
+		os.Exit(1)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/main.go` around lines 157 - 159, The os.Exit(1) call in the mgr.Start
error handling block bypasses deferred cleanup functions like tp.Shutdown and
auditLogger.Sync, causing telemetry data to be lost. Refactor the main function
by extracting the manager start logic into a helper function (e.g., run() that
returns an int error code) so that deferred telemetry cleanup executes before
os.Exit, or alternatively explicitly call tp.Shutdown() and auditLogger.Sync()
before os.Exit(1) in the error path after mgr.Start fails.

}
}

func initTracerProvider(endpoint string, insecure bool, log logr.Logger) *sdktrace.TracerProvider {
otel.SetTextMapPropagator(propagation.TraceContext{})

if endpoint == "" {
tp := sdktrace.NewTracerProvider()
otel.SetTracerProvider(tp)
log.Info("OTEL tracer: no-op (no endpoint configured)")
return tp
}

ctx := context.Background()
opts := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint(endpoint),
}
if insecure {
opts = append(opts, otlptracegrpc.WithInsecure())
}
exporter, err := otlptracegrpc.New(ctx, opts...)
if err != nil {
log.Error(err, "failed to create OTLP exporter, falling back to no-op")
tp := sdktrace.NewTracerProvider()
otel.SetTracerProvider(tp)
return tp
}

res, mergeErr := resource.Merge(
resource.Default(),
resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName("agentic-operator")),
)
if mergeErr != nil {
log.Error(mergeErr, "failed to merge OTEL resources, falling back to default resource")
res = resource.Default()
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
log.Info("OTEL tracer: OTLP exporter configured", "endpoint", endpoint, "insecure", insecure)
return tp
}
36 changes: 35 additions & 1 deletion config/crd/bases/agentic.openshift.io_agenticolsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ spec:
CR exists, the system behaves as if\nsuspended is false — the CR is not
required for normal operation.\n\nExample:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind:
AgenticOLSConfig\n\tmetadata:\n\t name: cluster\n\tspec:\n\t suspended:
false"
false\n\t audit:\n\t logging: true\n\t otel:\n\t endpoint: \"jaeger-otlp-grpc.observability.svc:4317\""
properties:
apiVersion:
description: |-
Expand All @@ -54,6 +54,40 @@ spec:
description: spec defines the desired system configuration.
minProperties: 1
properties:
audit:
description: |-
audit controls compliance audit logging. When absent, audit logging
is enabled by default with no OTEL export (structured JSON to stdout only).
minProperties: 1
properties:
logging:
description: |-
logging controls whether structured JSON audit events are emitted to stdout.
Defaults to true. When the CR is absent or this field is not set, audit
logging is enabled. Set to false to disable structured audit log output.
type: boolean
otel:
description: |-
otel holds the optional OTEL exporter configuration for distributed tracing.
Independent of the logging flag — tracing is enabled when an endpoint is set,
regardless of whether logging is on or off.
minProperties: 1
properties:
endpoint:
description: |-
endpoint is the OTLP gRPC endpoint URL (e.g. "jaeger-otlp-grpc.observability.svc:4317").
When empty or absent, a no-op exporter is used — no traces are sent.
maxLength: 253
minLength: 1
type: string
insecure:
description: |-
insecure disables TLS for the OTLP gRPC connection. Defaults to true
for backward compatibility with in-cluster collectors. Set to false
to require TLS when connecting to external or production endpoints.
type: boolean
type: object
type: object
suspended:
default: false
description: |-
Expand Down
13 changes: 13 additions & 0 deletions controller/proposal/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ func isStageDenied(approval *agenticv1alpha1.ProposalApproval, stage agenticv1al
return false
}

// isAutoApproved returns true if the policy grants automatic approval for the stage.
func isAutoApproved(policy *agenticv1alpha1.ApprovalPolicy, stage agenticv1alpha1.SandboxStep) bool {
if policy == nil {
return false
}
for _, ps := range policy.Spec.Stages {
if ps.Name == stage && ps.Approval == agenticv1alpha1.ApprovalModeAutomatic {
return true
}
}
return false
}

func getStageOverrideAgent(approval *agenticv1alpha1.ProposalApproval, stage agenticv1alpha1.SandboxStep) string {
if approval == nil {
return ""
Expand Down
Loading