Skip to content

Commit 667405d

Browse files
committed
feat: add per-proposal timeoutMinutes and configurable sandbox timeout
1 parent b36b7b0 commit 667405d

11 files changed

Lines changed: 149 additions & 109 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Binaries and local tool installs (Makefile uses ./bin for controller-gen, kustomize, etc.)
22
/bin/
3+
/oc-agentic
34

45
# IDE / OS
56
.idea/

config/crd/bases/agentic.openshift.io_proposals.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,28 @@ spec:
481481
x-kubernetes-validations:
482482
- message: schema is required when mode is Minimal
483483
rule: self.mode != 'Minimal' || has(self.schema)
484+
dataSource:
485+
description: |-
486+
dataSource references a PVC containing pre-populated input data
487+
(e.g., must-gather bundles, diagnostic data). The operator mounts
488+
it read-only at /data/input in the sandbox pod. Skills discover
489+
input data at this standard location.
490+
491+
Immutable: input data source is fixed at creation.
492+
properties:
493+
claimName:
494+
description: |-
495+
claimName is the name of the PersistentVolumeClaim to mount.
496+
The PVC must exist in the same namespace as the Proposal.
497+
maxLength: 253
498+
minLength: 1
499+
type: string
500+
x-kubernetes-validations:
501+
- message: must be a valid DNS subdomain
502+
rule: '!format.dns1123Subdomain().validate(self).hasValue()'
503+
required:
504+
- claimName
505+
type: object
484506
execution:
485507
description: |-
486508
execution defines per-step configuration for the execution step.
@@ -913,6 +935,19 @@ spec:
913935
x-kubernetes-validations:
914936
- message: each namespace must be a valid DNS label
915937
rule: self.all(ns, !format.dns1123Label().validate(ns).hasValue())
938+
timeoutMinutes:
939+
description: |-
940+
timeoutMinutes sets the per-step timeout for sandbox agent calls.
941+
This controls how long the operator waits for the sandbox pod to
942+
become ready and for the agent to complete its work. Increase this
943+
for long-running tools (e.g., IntelliAide RCA takes 10-30 minutes).
944+
Defaults to 5 minutes when omitted.
945+
946+
Mutable: can be adjusted before approving a step.
947+
format: int32
948+
maximum: 60
949+
minimum: 1
950+
type: integer
916951
tools:
917952
description: |-
918953
tools defines the default tools for all steps: skills images,
@@ -1684,6 +1719,9 @@ spec:
16841719
- message: verification is immutable once set
16851720
rule: '!has(oldSelf.verification) || (has(self.verification) && self.verification
16861721
== oldSelf.verification)'
1722+
- message: dataSource is immutable once set
1723+
rule: '!has(oldSelf.dataSource) || (has(self.dataSource) && self.dataSource
1724+
== oldSelf.dataSource)'
16871725
status:
16881726
description: status defines the observed state of Proposal.
16891727
minProperties: 1

config/rbac/role.yaml

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ kind: ClusterRole
44
metadata:
55
name: agentic-operator-manager-role
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- persistentvolumeclaims
11+
verbs:
12+
- get
13+
- list
14+
- watch
715
- apiGroups:
816
- agentic.openshift.io
917
resources:
@@ -58,35 +66,6 @@ rules:
5866
- proposals/finalizers
5967
verbs:
6068
- update
61-
- apiGroups:
62-
- agents.x-k8s.io
63-
resources:
64-
- sandboxes
65-
verbs:
66-
- get
67-
- list
68-
- watch
69-
- apiGroups:
70-
- extensions.agents.x-k8s.io
71-
resources:
72-
- sandboxclaims
73-
verbs:
74-
- create
75-
- delete
76-
- get
77-
- list
78-
- watch
79-
- apiGroups:
80-
- extensions.agents.x-k8s.io
81-
resources:
82-
- sandboxtemplates
83-
verbs:
84-
- create
85-
- delete
86-
- get
87-
- list
88-
- update
89-
- watch
9069
- apiGroups:
9170
- rbac.authorization.k8s.io
9271
resources:

controller/proposal/agent.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proposal
22

33
import (
44
"context"
5+
"time"
56

67
agenticv1alpha1 "github.com/openshift/lightspeed-agentic-operator/api/v1alpha1"
78
)
@@ -42,18 +43,18 @@ type EscalationOutput struct {
4243
// HTTP implementations POST to /v1/agent/run — a step-agnostic
4344
// endpoint where all workflow context is in the request payload.
4445
type AgentCaller interface {
45-
Analyze(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, requestText string) (*AnalysisOutput, error)
46-
Execute(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, option *agenticv1alpha1.RemediationOption) (*ExecutionOutput, error)
47-
Verify(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, option *agenticv1alpha1.RemediationOption, exec *ExecutionOutput) (*VerificationOutput, error)
48-
Escalate(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, requestText string) (*EscalationOutput, error)
46+
Analyze(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, requestText string, timeout time.Duration) (*AnalysisOutput, error)
47+
Execute(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, option *agenticv1alpha1.RemediationOption, timeout time.Duration) (*ExecutionOutput, error)
48+
Verify(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, option *agenticv1alpha1.RemediationOption, exec *ExecutionOutput, timeout time.Duration) (*VerificationOutput, error)
49+
Escalate(ctx context.Context, proposal *agenticv1alpha1.Proposal, step resolvedStep, requestText string, timeout time.Duration) (*EscalationOutput, error)
4950
ReleaseSandboxes(ctx context.Context, proposal *agenticv1alpha1.Proposal) error
5051
}
5152

5253
// StubAgentCaller returns canned success results. Wire in a real
5354
// implementation (sandbox + HTTP) when the agent infrastructure is ready.
5455
type StubAgentCaller struct{}
5556

56-
func (s *StubAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string) (*AnalysisOutput, error) {
57+
func (s *StubAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string, _ time.Duration) (*AnalysisOutput, error) {
5758
return &AnalysisOutput{
5859
Success: true,
5960
Options: []agenticv1alpha1.RemediationOption{{
@@ -73,7 +74,7 @@ func (s *StubAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal
7374
}, nil
7475
}
7576

76-
func (s *StubAgentCaller) Execute(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption) (*ExecutionOutput, error) {
77+
func (s *StubAgentCaller) Execute(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ time.Duration) (*ExecutionOutput, error) {
7778
return &ExecutionOutput{
7879
Success: true,
7980
ActionsTaken: []agenticv1alpha1.ExecutionAction{{
@@ -88,7 +89,7 @@ func (s *StubAgentCaller) Execute(_ context.Context, _ *agenticv1alpha1.Proposal
8889
}, nil
8990
}
9091

91-
func (s *StubAgentCaller) Escalate(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string) (*EscalationOutput, error) {
92+
func (s *StubAgentCaller) Escalate(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string, _ time.Duration) (*EscalationOutput, error) {
9293
return &EscalationOutput{
9394
Success: true,
9495
Summary: "Stub escalation summary",
@@ -100,7 +101,7 @@ func (s *StubAgentCaller) ReleaseSandboxes(_ context.Context, _ *agenticv1alpha1
100101
return nil
101102
}
102103

103-
func (s *StubAgentCaller) Verify(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ *ExecutionOutput) (*VerificationOutput, error) {
104+
func (s *StubAgentCaller) Verify(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ *ExecutionOutput, _ time.Duration) (*VerificationOutput, error) {
104105
return &VerificationOutput{
105106
Success: true,
106107
Checks: []agenticv1alpha1.VerifyCheck{{

controller/proposal/client.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,13 @@ type AgentHTTPClient struct {
7474
endpoint string
7575
}
7676

77-
func NewAgentHTTPClient(endpoint string) AgentHTTPClientInterface {
77+
func NewAgentHTTPClient(endpoint string, timeout time.Duration) AgentHTTPClientInterface {
78+
if timeout <= 0 {
79+
timeout = defaultSandboxTimeout
80+
}
7881
return &AgentHTTPClient{
7982
httpClient: &http.Client{
80-
Timeout: 5 * time.Minute,
83+
Timeout: timeout,
8184
Transport: &http.Transport{
8285
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // internal cluster traffic
8386
},
@@ -87,11 +90,13 @@ func NewAgentHTTPClient(endpoint string) AgentHTTPClientInterface {
8790
}
8891

8992
func (c *AgentHTTPClient) Run(ctx context.Context, systemPrompt, query string, outputSchema json.RawMessage, agentCtx *agentContext) (*agentRunResponse, error) {
93+
timeoutMs := int64(c.httpClient.Timeout / time.Millisecond)
9094
req := agentRunRequest{
9195
Query: query,
9296
SystemPrompt: systemPrompt,
9397
OutputSchema: outputSchema,
9498
Context: agentCtx,
99+
TimeoutMs: &timeoutMs,
95100
}
96101

97102
body, err := json.Marshal(req)

controller/proposal/client_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestAgentHTTPClient_RunSuccess(t *testing.T) {
3535
}))
3636
defer server.Close()
3737

38-
client := NewAgentHTTPClient(server.URL)
38+
client := NewAgentHTTPClient(server.URL, 0)
3939
resp, err := client.Run(context.Background(), "You are an SRE agent", "check health", nil, nil)
4040
if err != nil {
4141
t.Fatalf("unexpected error: %v", err)
@@ -52,15 +52,15 @@ func TestAgentHTTPClient_RunHTTPError(t *testing.T) {
5252
}))
5353
defer server.Close()
5454

55-
client := NewAgentHTTPClient(server.URL)
55+
client := NewAgentHTTPClient(server.URL, 0)
5656
_, err := client.Run(context.Background(), "", "test", nil, nil)
5757
if err == nil {
5858
t.Fatal("expected error for HTTP 500")
5959
}
6060
}
6161

6262
func TestAgentHTTPClient_RunConnectionError(t *testing.T) {
63-
client := NewAgentHTTPClient("http://127.0.0.1:1")
63+
client := NewAgentHTTPClient("http://127.0.0.1:1", 0)
6464
_, err := client.Run(context.Background(), "", "test", nil, nil)
6565
if err == nil {
6666
t.Fatal("expected error for connection failure")
@@ -100,7 +100,7 @@ func TestAgentHTTPClient_RunWithExecutionResult(t *testing.T) {
100100
}))
101101
defer server.Close()
102102

103-
client := NewAgentHTTPClient(server.URL)
103+
client := NewAgentHTTPClient(server.URL, 0)
104104
agentCtx := &agentContext{
105105
TargetNamespaces: []string{"production"},
106106
ExecutionResult: &agentExecutionResult{
@@ -135,7 +135,7 @@ func TestAgentHTTPClient_RunWithoutExecutionResult(t *testing.T) {
135135
}))
136136
defer server.Close()
137137

138-
client := NewAgentHTTPClient(server.URL)
138+
client := NewAgentHTTPClient(server.URL, 0)
139139
agentCtx := &agentContext{
140140
TargetNamespaces: []string{"production"},
141141
}
@@ -169,7 +169,7 @@ func TestAgentHTTPClient_RunWithContext(t *testing.T) {
169169
}))
170170
defer server.Close()
171171

172-
client := NewAgentHTTPClient(server.URL)
172+
client := NewAgentHTTPClient(server.URL, 0)
173173
agentCtx := &agentContext{
174174
TargetNamespaces: []string{"production"},
175175
PreviousAttempts: []agentPreviousAttempt{{Attempt: 1, FailureReason: "timeout"}},

controller/proposal/handlers.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ func (r *ProposalReconciler) handleAnalysis(
5858
return ctrl.Result{}, fmt.Errorf("update to Analyzing: %w", err)
5959
}
6060

61-
analysisResult, err := r.Agent.Analyze(ctx, proposal, resolved.Analysis, proposal.Spec.Request)
61+
timeout := proposalTimeout(proposal)
62+
analysisResult, err := r.Agent.Analyze(ctx, proposal, resolved.Analysis, proposal.Spec.Request, timeout)
6263
if err != nil {
6364
return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, err)
6465
}
@@ -124,7 +125,8 @@ func (r *ProposalReconciler) handleRevision(
124125
revisionSuffix := buildRevisionContext(proposal)
125126
requestWithRevision := proposal.Spec.Request + "\n\n" + revisionSuffix
126127

127-
analysisResult, err := r.Agent.Analyze(ctx, proposal, resolved.Analysis, requestWithRevision)
128+
timeout := proposalTimeout(proposal)
129+
analysisResult, err := r.Agent.Analyze(ctx, proposal, resolved.Analysis, requestWithRevision, timeout)
128130
if err != nil {
129131
return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionAnalyzed, err)
130132
}
@@ -240,7 +242,8 @@ func (r *ProposalReconciler) handleExecution(
240242
return ctrl.Result{}, fmt.Errorf("update to Executing: %w", err)
241243
}
242244

243-
execResult, err := r.Agent.Execute(ctx, proposal, *resolved.Execution, selectedOption)
245+
timeout := proposalTimeout(proposal)
246+
execResult, err := r.Agent.Execute(ctx, proposal, *resolved.Execution, selectedOption, timeout)
244247
if err != nil {
245248
return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionExecuted, err)
246249
}
@@ -343,7 +346,8 @@ func (r *ProposalReconciler) handleVerification(
343346
}
344347
}
345348

346-
verifyResult, err := r.Agent.Verify(ctx, proposal, *resolved.Verification, selectedOption, execOutput)
349+
timeout := proposalTimeout(proposal)
350+
verifyResult, err := r.Agent.Verify(ctx, proposal, *resolved.Verification, selectedOption, execOutput, timeout)
347351
if err != nil {
348352
return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionVerified, err)
349353
}
@@ -503,7 +507,8 @@ func (r *ProposalReconciler) handleEscalation(
503507
}
504508

505509
escalationText := buildEscalationRequest(proposal)
506-
escalationResult, err := r.Agent.Escalate(ctx, proposal, step, escalationText)
510+
timeout := proposalTimeout(proposal)
511+
escalationResult, err := r.Agent.Escalate(ctx, proposal, step, escalationText, timeout)
507512
if err != nil {
508513
return r.failStep(ctx, log, proposal, agenticv1alpha1.ProposalConditionEscalated, err)
509514
}

controller/proposal/reconciler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ProposalReconciler struct {
4141
// +kubebuilder:rbac:groups=agentic.openshift.io,resources=analysisresults/status;executionresults/status;verificationresults/status;escalationresults/status,verbs=get;patch;update
4242
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;create;delete
4343
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;create;delete
44+
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch
4445

4546
func (r *ProposalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
4647
log := r.Log.WithValues("proposal", req.NamespacedName)

controller/proposal/reconciler_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,32 +36,32 @@ type testAgentCaller struct {
3636

3737
func newTestAgentCaller() *testAgentCaller {
3838
stub := &StubAgentCaller{}
39-
a, _ := stub.Analyze(context.Background(), nil, resolvedStep{}, "")
40-
e, _ := stub.Execute(context.Background(), nil, resolvedStep{}, nil)
41-
v, _ := stub.Verify(context.Background(), nil, resolvedStep{}, nil, nil)
42-
esc, _ := stub.Escalate(context.Background(), nil, resolvedStep{}, "")
39+
a, _ := stub.Analyze(context.Background(), nil, resolvedStep{}, "", 0)
40+
e, _ := stub.Execute(context.Background(), nil, resolvedStep{}, nil, 0)
41+
v, _ := stub.Verify(context.Background(), nil, resolvedStep{}, nil, nil, 0)
42+
esc, _ := stub.Escalate(context.Background(), nil, resolvedStep{}, "", 0)
4343
return &testAgentCaller{analyzeResult: a, executeResult: e, verifyResult: v, escalateResult: esc}
4444
}
4545

46-
func (ta *testAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string) (*AnalysisOutput, error) {
46+
func (ta *testAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string, _ time.Duration) (*AnalysisOutput, error) {
4747
if ta.analyzeErr != nil {
4848
return nil, ta.analyzeErr
4949
}
5050
return ta.analyzeResult, nil
5151
}
52-
func (ta *testAgentCaller) Execute(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption) (*ExecutionOutput, error) {
52+
func (ta *testAgentCaller) Execute(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ time.Duration) (*ExecutionOutput, error) {
5353
if ta.executeErr != nil {
5454
return nil, ta.executeErr
5555
}
5656
return ta.executeResult, nil
5757
}
58-
func (ta *testAgentCaller) Verify(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ *ExecutionOutput) (*VerificationOutput, error) {
58+
func (ta *testAgentCaller) Verify(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ *agenticv1alpha1.RemediationOption, _ *ExecutionOutput, _ time.Duration) (*VerificationOutput, error) {
5959
if ta.verifyErr != nil {
6060
return nil, ta.verifyErr
6161
}
6262
return ta.verifyResult, nil
6363
}
64-
func (ta *testAgentCaller) Escalate(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string) (*EscalationOutput, error) {
64+
func (ta *testAgentCaller) Escalate(_ context.Context, _ *agenticv1alpha1.Proposal, _ resolvedStep, _ string, _ time.Duration) (*EscalationOutput, error) {
6565
if ta.escalateErr != nil {
6666
return nil, ta.escalateErr
6767
}
@@ -248,7 +248,7 @@ func newMockSandboxAgent(analysisJSON, executionJSON, verificationJSON string) (
248248
caller := &SandboxAgentCaller{
249249
Sandbox: sandbox,
250250
K8sClient: fc,
251-
ClientFactory: func(_ string) AgentHTTPClientInterface {
251+
ClientFactory: func(_ string, _ time.Duration) AgentHTTPClientInterface {
252252
resp := responses[callCount%len(responses)]
253253
callCount++
254254
httpClient.response = &agentRunResponse{Response: json.RawMessage(resp)}

0 commit comments

Comments
 (0)