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
2 changes: 1 addition & 1 deletion .ai/spec/what/crd-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Kubernetes API surface for the agentic operator. **Lifecycle and gates** are in
31. **ExecutionResult**: Adds `spec.retryIndex` (bound to allowed retry range per field validation); `status.actionsTaken`, `status.verification` (inline), optional `failureReason`, `sandbox`.
32. **VerificationResult**: `spec.retryIndex` parallels execution for the same attempt cluster; `status.checks`, `status.summary`, optional `failureReason`, `sandbox`.
33. **EscalationResult**: `status.summary`, `status.content`, optional `failureReason`, `sandbox`; no `retryIndex`.
34. **RemediationOption**: Cohesion rules require `diagnosis` and `remediationPlan` to be paired when present; `components` holds schemaless JSON for adapter data shaped by `spec.analysisOutput.schema`.
34. **RemediationOption**: Cohesion rules require `diagnosis` and `remediationPlan` to be paired when present; `components` holds schemaless JSON for adapter data shaped by `spec.analysisOutput.schema`. Each action in `remediationPlan.actions` includes `command` (exact bash command using kubectl/oc), `type` (pre-check, mutation, wait, post-check), and `description`. [OLS-3441]
35. **RBACResult / RBACRule**: Analysis MAY request namespace-scoped and cluster-scoped rules with verb/apigroup/resource metadata and mandatory `justification`; `namespace` on rules MUST align with run targeting rules (validated at runtime by policy engine per field comments).
36. **ToolsSpec**: MAY include `skills` (unique images), `mcpServers` (unique names), `requiredSecrets` (unique names). `SkillsSource.image` MUST be a valid pullspec; optional `paths` restrict mounted subtrees.
37. **SecretRequirement**: Names a namespace-local `Secret`; `mountAs` discriminates `EnvVar` vs `FilePath` with required nested config per type.
Expand Down
2 changes: 1 addition & 1 deletion .ai/spec/what/sandbox-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Behavioral specification for how workflow steps run inside ephemeral **sandboxes
9. **Response handling**: HTTP success responses MUST be parsed as JSON matching the per-step schema (analysis/execution/verification/escalation). Non-success HTTP MUST fail the step with an error surfaced to run conditions.
10. **Output schema selection**: `outputSchema` MUST be the step-specific JSON schema: analysis schema depends on `spec.analysisOutput.mode`, whether execution/verification steps exist in the run, and optional injected `components` sub-schema from `spec.analysisOutput.schema`; other steps use fixed schemas for their response shapes.
11. **Analysis query payload**: The `query` string MUST encode the user request or revision-augmented request and encode workflow flags indicating whether execution/verification steps exist (template-rendered).
12. **Execution query payload**: The `query` MUST include JSON describing the approved remediation option.
12. **Execution query payload**: The `query` MUST include JSON describing the approved remediation option, which contains a concrete remediation script (ordered bash commands). The execution prompt MUST instruct the agent to follow the script exactly.
13. **Verification query payload**: The `query` MUST include the approved option JSON and a JSON description of the latest execution output (actions and inline verification) when available.
14. **Context envelope**: The `context` object MUST include `targetNamespaces` from `spec.targetNamespaces`, synthesized `previousAttempts` from failed prior `status.steps.*.results` entries, `approvedOption` when executing/verifying, and `executionResult` when verifying. Note: the sandbox context prefix formatter (see sandbox `run-api.md`) only expands `targetNamespaces`, `attempt`, `previousAttempts`, and `approvedOption` into the model prompt; `executionResult` is carried in `context` for tracing but verification execution details are primarily conveyed to the model via the `query` body (rendered from the verification template).
15. **Secrets — run** `spec.tools.requiredSecrets` / per-step tools: Secret objects MUST live in the **same namespace as the `AgenticRun`**. Mounting into sandbox MUST honor `SecretMountSpec`: environment variable injection OR file mount at configured absolute path.
Expand Down
23 changes: 16 additions & 7 deletions api/v1alpha1/proposal_analysis_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,27 @@ type DiagnosisResult struct {
}

// 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.
// recommends as part of its remediation plan. Each action contains an
// exact executable bash command and is displayed to the user after
// analysis for review before approval.
type ProposedAction struct {
// 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.
// command is the exact executable bash command using kubectl or oc
// (e.g., "kubectl set image deployment/foo container=registry/foo:v1.3 -n production").
// Must be a concrete command that can be copy-pasted and run.
// Maximum 4096 characters.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=4096
Command string `json:"command,omitempty"`
// type is the action phase category: "pre-check", "mutation", "wait",
// or "post-check". Free-form string to allow agents to express
// domain-specific action types. Must be 1-256 characters.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=256
Type string `json:"type,omitempty"`
// description is a Markdown-formatted explanation of what this action
// will do (e.g., "Increase memory limit from 256Mi to 512Mi").
// description is a Markdown-formatted explanation of what this command
// does and why (e.g., "Increase memory limit from 256Mi to 512Mi").
// Maximum 4096 characters.
// +required
// +kubebuilder:validation:MinLength=1
Expand Down
25 changes: 18 additions & 7 deletions config/crd/bases/agentic.openshift.io_analysisresults.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -209,26 +209,37 @@ spec:
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.
recommends as part of its remediation plan. Each action contains an
exact executable bash command and is displayed to the user after
analysis for review before approval.
properties:
command:
description: |-
command is the exact executable bash command using kubectl or oc
(e.g., "kubectl set image deployment/foo container=registry/foo:v1.3 -n production").
Must be a concrete command that can be copy-pasted and run.
Maximum 4096 characters.
maxLength: 4096
minLength: 1
type: string
description:
description: |-
description is a Markdown-formatted explanation of what this action
will do (e.g., "Increase memory limit from 256Mi to 512Mi").
description is a Markdown-formatted explanation of what this command
does and why (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.
type is the action phase category: "pre-check", "mutation", "wait",
or "post-check". Free-form string to allow agents to express
domain-specific action types. Must be 1-256 characters.
maxLength: 256
minLength: 1
type: string
required:
- command
- description
- type
type: object
Expand Down
2 changes: 1 addition & 1 deletion controller/proposal/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (s *StubAgentCaller) Analyze(_ context.Context, _ *agenticv1alpha1.Proposal
},
Proposal: agenticv1alpha1.ProposalResult{
Description: "Stub proposal",
Actions: []agenticv1alpha1.ProposedAction{{Type: "stub", Description: "Stub action"}},
Actions: []agenticv1alpha1.ProposedAction{{Command: "kubectl get pods -n default", Type: "pre-check", Description: "Stub action"}},
Risk: "Low",
Reversible: agenticv1alpha1.ReversibilityReversible,
},
Expand Down
6 changes: 3 additions & 3 deletions controller/proposal/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ func TestReconcile_ExecutionRBACCreatedOnApproval(t *testing.T) {
},
Proposal: agenticv1alpha1.ProposalResult{
Description: "Increase to 512Mi",
Actions: []agenticv1alpha1.ProposedAction{{Type: "patch", Description: "Patch deploy"}},
Actions: []agenticv1alpha1.ProposedAction{{Command: "kubectl patch deployment/web -n production -p '{}'", Type: "mutation", Description: "Patch deploy"}},
Risk: "Low",
Reversible: agenticv1alpha1.ReversibilityReversible,
},
Expand Down Expand Up @@ -777,7 +777,7 @@ func TestReconcile_ExecutionRBACCleanedOnFailure(t *testing.T) {
},
Proposal: agenticv1alpha1.ProposalResult{
Description: "Apply fix",
Actions: []agenticv1alpha1.ProposedAction{{Type: "patch", Description: "Patch"}},
Actions: []agenticv1alpha1.ProposedAction{{Command: "kubectl patch deployment/web -n production -p '{}'", Type: "mutation", Description: "Patch"}},
Risk: "Low",
Reversible: agenticv1alpha1.ReversibilityReversible,
},
Expand Down Expand Up @@ -837,7 +837,7 @@ func TestFullLifecycle_WithSandboxAgent(t *testing.T) {
},
Proposal: agenticv1alpha1.ProposalResult{
Description: "Increase deployment memory limit to 512Mi",
Actions: []agenticv1alpha1.ProposedAction{{Type: "patch", Description: "Patch deployment memory limit"}},
Actions: []agenticv1alpha1.ProposedAction{{Command: "kubectl patch deployment/web -n production -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"web\",\"resources\":{\"limits\":{\"memory\":\"512Mi\"}}}]}}}}'", Type: "mutation", Description: "Patch deployment memory limit"}},
Risk: "Low",
Reversible: agenticv1alpha1.ReversibilityReversible,
},
Expand Down
18 changes: 9 additions & 9 deletions controller/proposal/revision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ func TestBuildAnalysisQuery_FullProposal(t *testing.T) {
},
}
result := buildAnalysisQuery("Fix the crash", proposal)
if !strings.Contains(result, "RBAC permissions") {
t.Error("full proposal should mention RBAC permissions")
if !strings.Contains(result, "Derive RBAC") {
t.Error("full proposal should mention RBAC derivation")
}
if !strings.Contains(result, "verification plan") {
if !strings.Contains(result, "Verification plan") {
t.Error("full proposal should mention verification plan")
}
if !strings.Contains(result, "Fix the crash") {
Expand All @@ -104,21 +104,21 @@ func TestBuildAnalysisQuery_TrustMode(t *testing.T) {
},
}
result := buildAnalysisQuery("Fix the crash", proposal)
if !strings.Contains(result, "RBAC permissions") {
t.Error("execution proposal should mention RBAC permissions")
if !strings.Contains(result, "Derive RBAC") {
t.Error("execution proposal should mention RBAC derivation")
}
if strings.Contains(result, "verification plan") {
if strings.Contains(result, "Verification plan") {
t.Error("trust-mode proposal should NOT mention verification plan")
}
}

func TestBuildAnalysisQuery_Advisory(t *testing.T) {
proposal := &agenticv1alpha1.Proposal{}
result := buildAnalysisQuery("What is 2+2?", proposal)
if strings.Contains(result, "RBAC permissions") {
t.Error("advisory proposal should NOT mention RBAC permissions")
if strings.Contains(result, "Derive RBAC") {
t.Error("advisory proposal should NOT mention RBAC derivation")
}
if strings.Contains(result, "verification plan") {
if strings.Contains(result, "Verification plan") {
t.Error("advisory proposal should NOT mention verification plan")
}
if !strings.Contains(result, "What is 2+2?") {
Expand Down
17 changes: 13 additions & 4 deletions controller/proposal/sandbox_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestSandboxAgentCaller_Analyze_HappyPath(t *testing.T) {
sandbox := &mockSandboxProvider{claimName: "ls-analysis-fix-crash", endpoint: "http://sandbox:8080"}
httpClient := &mockHTTPClient{
response: &agentRunResponse{
Response: json.RawMessage(`{"success": true, "options": [{"title": "Increase memory", "diagnosis": {"summary": "OOM", "confidence": "High", "rootCause": "memory limit"}, "proposal": {"description": "Bump memory", "actions": [{"type": "patch", "description": "patch deploy"}], "risk": "Low"}}]}`),
Response: json.RawMessage(`{"success": true, "options": [{"title": "Increase memory", "diagnosis": {"summary": "OOM", "confidence": "High", "rootCause": "memory limit"}, "proposal": {"description": "Bump memory", "actions": [{"command": "kubectl patch deployment/web -n production -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"web\",\"resources\":{\"limits\":{\"memory\":\"512Mi\"}}}]}}}}'", "type": "mutation", "description": "patch deploy"}], "risk": "Low"}}]}`),
},
}

Expand Down Expand Up @@ -426,6 +426,12 @@ func TestSandboxAgentCaller_AnalysisQueryFraming(t *testing.T) {
if !strings.Contains(httpClient.lastQuery, "Do NOT attempt to fix") {
t.Error("analysis query should instruct agent not to execute")
}
if !strings.Contains(httpClient.lastQuery, "remediation script") {
t.Error("analysis query should require a remediation script")
}
if !strings.Contains(httpClient.lastQuery, "bash commands") {
t.Error("analysis query should reference bash commands")
}
}

func TestSandboxAgentCaller_ExecutionQueryFraming(t *testing.T) {
Expand All @@ -439,7 +445,7 @@ func TestSandboxAgentCaller_ExecutionQueryFraming(t *testing.T) {
Title: "Increase memory limit",
Proposal: agenticv1alpha1.ProposalResult{
Description: "Patch deployment memory",
Actions: []agenticv1alpha1.ProposedAction{{Type: "patch", Description: "Set memory to 512Mi"}},
Actions: []agenticv1alpha1.ProposedAction{{Command: "kubectl patch deployment/web -n production -p '{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"web\",\"resources\":{\"limits\":{\"memory\":\"512Mi\"}}}]}}}}'", Type: "mutation", Description: "Set memory to 512Mi"}},
Risk: "Low",
},
}
Expand All @@ -456,6 +462,9 @@ func TestSandboxAgentCaller_ExecutionQueryFraming(t *testing.T) {
if strings.Contains(httpClient.lastQuery, "Pod crashing with OOMKilled") {
t.Error("execution query should NOT contain the original request")
}
if !strings.Contains(httpClient.lastQuery, "exact bash commands") {
t.Error("execution query should reference exact bash commands")
}
}

func TestSandboxAgentCaller_VerificationQueryFraming(t *testing.T) {
Expand Down Expand Up @@ -513,7 +522,7 @@ func TestSandboxAgentCaller_Analyze_PatchesSandboxInfo(t *testing.T) {
sandbox := &mockSandboxProvider{claimName: "ls-analysis-fix-crash", endpoint: "http://sandbox:8080"}
httpClient := &mockHTTPClient{
response: &agentRunResponse{
Response: json.RawMessage(`{"success": true, "options": [{"title": "Fix it", "diagnosis": {"summary": "broken", "confidence": "High", "rootCause": "bug"}, "proposal": {"description": "fix", "actions": [{"type": "patch", "description": "patch"}], "risk": "Low"}}]}`),
Response: json.RawMessage(`{"success": true, "options": [{"title": "Fix it", "diagnosis": {"summary": "broken", "confidence": "High", "rootCause": "bug"}, "proposal": {"description": "fix", "actions": [{"command": "kubectl patch deployment/web -n default -p '{}'", "type": "mutation", "description": "patch"}], "risk": "Low"}}]}`),
},
}

Expand Down Expand Up @@ -542,7 +551,7 @@ func TestSandboxAgentCaller_Execute_PatchesSandboxInfo(t *testing.T) {
sandbox := &mockSandboxProvider{claimName: "ls-execution-fix-crash", endpoint: "http://sandbox:8080"}
httpClient := &mockHTTPClient{
response: &agentRunResponse{
Response: json.RawMessage(`{"success": true, "actionsTaken": [{"type": "patch", "description": "patched deploy"}]}`),
Response: json.RawMessage(`{"success": true, "actionsTaken": [{"type": "mutation", "description": "patched deploy"}]}`),
},
}

Expand Down
9 changes: 5 additions & 4 deletions controller/proposal/schemas.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ var AnalysisOutputSchema = json.RawMessage(`{
"description": { "type": "string", "description": "Markdown-formatted summary of the overall remediation approach" },
"actions": {
"type": "array",
"description": "Ordered list of discrete actions to perform",
"description": "Ordered list of exact bash commands to execute. Each action is one command.",
"items": {
"type": "object",
"properties": {
"type": { "type": "string", "description": "Action category (e.g., 'patch', 'scale', 'restart', 'create', 'delete', 'rollout')" },
"description": { "type": "string", "description": "What this action does (e.g., 'Increase memory limit from 256Mi to 512Mi')" }
"command": { "type": "string", "description": "Exact executable bash command using kubectl or oc (e.g., 'kubectl set image deployment/foo container=registry/foo:v1.3 -n production')" },
"type": { "type": "string", "description": "Action phase category (e.g., 'pre-check', 'mutation', 'wait', 'post-check')" },
"description": { "type": "string", "description": "What this command does and why" }
},
"required": ["type", "description"]
"required": ["command", "type", "description"]
}
},
"risk": { "type": "string", "enum": ["Low", "Medium", "High", "Critical"], "description": "Risk assessment. Low: safe to apply. Medium: review recommended. High: careful review required. Critical: manual approval strongly recommended" },
Expand Down
21 changes: 21 additions & 0 deletions controller/proposal/schemas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,27 @@ func TestAnalysisOutputSchema_OptionsStructure(t *testing.T) {
}
}

proposalProp := itemProps["proposal"].(map[string]any)
proposalProps := proposalProp["properties"].(map[string]any)
actionsProp := proposalProps["actions"].(map[string]any)
actionItems := actionsProp["items"].(map[string]any)
actionItemProps := actionItems["properties"].(map[string]any)
for _, key := range []string{"command", "type", "description"} {
if _, ok := actionItemProps[key]; !ok {
t.Errorf("proposal.actions items missing property %q", key)
}
}
actionRequired := actionItems["required"].([]any)
actionRequiredSet := map[string]bool{}
for _, r := range actionRequired {
actionRequiredSet[r.(string)] = true
}
for _, key := range []string{"command", "type", "description"} {
if !actionRequiredSet[key] {
t.Errorf("%q should be required in proposal.actions items", key)
}
}

required := items["required"].([]any)
requiredSet := map[string]bool{}
for _, r := range required {
Expand Down
Loading