Skip to content

Commit fb32b28

Browse files
authored
feat(augment): Phase 5 - SonataFlow agent-approval workflow (#3226)
* feat(augment): add polling to admin dashboards without loading flash Phase 3 of lifecycle hardening: adds 30-second polling to ReviewQueue and OpsOverview for real-time admin visibility. Key fix: only sets loading=true on the initial load, not on subsequent polls. This eliminates the skeleton flash that occurred every 30 seconds in the previous implementation (PR G). ReviewQueue: - 30s polling interval for review queue agents - initialLoadDone ref prevents loading flash on polls OpsOverview: - 30s polling interval for agent and tool data - Same loading flash fix via initialLoadDone ref * feat(augment): enterprise-grade cascading delete and lifecycle enforcement Phase 4 of lifecycle hardening: source-aware cascading delete and lifecycle graph enforcement for publish routes. Cascading DELETE /agents/:agentId: - Detects agent source via unified agent list (kagenti/orchestration/workflow) - For orchestration agents: removes from 'agents' admin config key AND chatAgents lifecycle entry in a single operation - For Kagenti agents: removes chatAgents entry, notes that K8s cleanup requires the dedicated admin endpoint - Returns detailed cleanupResults per store for transparency - Ownership enforcement: non-admins restricted to own draft agents Lifecycle enforcement on publish routes: - PUT /agents/:agentId/publish: detects when admin bypasses lifecycle stages (e.g. draft -> production), logs audit warning with lifecycleBypassed flag, still allows the operation - PUT /agents/bulk-publish: same bypass detection per agent, logs warning with count of bypassed agents OrchAgentDetailView: - Updated handleDelete to use the cascading DELETE /agents/:agentId endpoint instead of directly mutating admin config - Removed unused useAdminConfig('agents') hook * feat(augment): add SonataFlow agent-approval workflow Phase 5 of lifecycle hardening: SonataFlow workflow for automated agent lifecycle approval, based on the RHDH orchestrator escalation pattern. Workflow (agent-approval.sw.yaml): - Triggered when agent is submitted for review (draft -> review) - Sends Backstage notification to admins on submission - Suspends in callback state awaiting admin decision CloudEvent - Approval: promotes agent to staging, notifies creator - Rejection: demotes to draft with reason, notifies creator - 72-hour timeout: sends escalation notification, re-enters wait state - Uses kogitoprocrefid CloudEvent extension for instance correlation Infrastructure: - OpenAPI spec for augment promote/demote endpoints - JSON Schema for workflow input validation - Application properties for SonataFlow Operator deployment - Knative Eventing configuration for CloudEvent routing CloudEvent type: io.rhdhorchestrator.agent.approval.decision Correlation: kogitoprocrefid (SonataFlow process instance ID) Prerequisites: SonataFlow Operator, Knative Eventing, Backstage Notifications plugin, OIDC service account.
1 parent 1ab1689 commit fb32b28

5 files changed

Lines changed: 393 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Agent Lifecycle Approval Workflow
2+
3+
SonataFlow workflow that automates the agent lifecycle approval process in RHDH.
4+
5+
## Overview
6+
7+
When an agent is submitted for review (`draft -> review`), this workflow:
8+
9+
1. **Notifies admins** via Backstage Notifications that a new agent is pending review
10+
2. **Suspends** in a `callback` state, waiting for an admin decision CloudEvent
11+
3. On **approval**: promotes the agent to `staging` and notifies the creator
12+
4. On **rejection**: demotes the agent to `draft` with a reason and notifies the creator
13+
5. On **timeout** (72 hours): sends an escalation notification and re-enters the wait state
14+
15+
## Architecture
16+
17+
```
18+
Agent Creator SonataFlow Admin
19+
| | |
20+
|-- submit for review -------->| |
21+
| |-- notify admins ----------->|
22+
| | |
23+
| | (callback state) |
24+
| | waits for CloudEvent |
25+
| | |
26+
| |<-- approval decision -------|
27+
| | (CloudEvent with |
28+
| | kogitoprocrefid) |
29+
| | |
30+
|<-- approval notification ----| |
31+
| |-- promote to staging ------>|
32+
```
33+
34+
## CloudEvent Format
35+
36+
The admin decision CloudEvent must include:
37+
38+
```json
39+
{
40+
"specversion": "1.0",
41+
"type": "io.rhdhorchestrator.agent.approval.decision",
42+
"source": "augment.admin",
43+
"id": "<unique-event-id>",
44+
"kogitoprocrefid": "<workflow-instance-id>",
45+
"datacontenttype": "application/json",
46+
"data": {
47+
"approved": true,
48+
"decidedBy": "user:default/admin-name",
49+
"reason": "Optional reason (required for rejections)"
50+
}
51+
}
52+
```
53+
54+
## Prerequisites
55+
56+
- SonataFlow Operator installed on OpenShift
57+
- Knative Eventing with a Broker configured
58+
- Backstage Notifications plugin enabled
59+
- OIDC client configured for service-to-service auth
60+
61+
## Deployment
62+
63+
1. Create a `SonataFlow` CR referencing this workflow
64+
2. Configure environment variables in `application.properties`
65+
3. Set up a Knative Trigger to route `io.rhdhorchestrator.agent.approval.decision` events to the workflow
66+
4. Configure the Augment backend to emit CloudEvents on admin approve/reject actions
67+
68+
## Configuration
69+
70+
| Variable | Description | Default |
71+
| ---------------------- | ------------------------------- | ----------------------------------------------------------------------------- |
72+
| `NOTIFICATIONS_URL` | Backstage Notifications API URL | `http://backstage-backend.backstage.svc.cluster.local:7007/api/notifications` |
73+
| `AUGMENT_BACKEND_URL` | Augment plugin backend URL | `http://backstage-backend.backstage.svc.cluster.local:7007/api/augment` |
74+
| `K_SINK` | Knative Eventing sink URL | `http://broker-ingress.knative-eventing.svc.cluster.local/default/default` |
75+
| `OIDC_CLIENT_ID` | OIDC client ID for service auth | `sonataflow-agent-approval` |
76+
| `OIDC_CLIENT_SECRET` | OIDC client secret | (required) |
77+
| `OIDC_AUTH_SERVER_URL` | Keycloak/OIDC server URL | (required) |
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
specVersion: '0.8'
2+
id: agentApproval
3+
name: Agent Lifecycle Approval
4+
annotations:
5+
- 'workflow-type/infrastructure'
6+
version: 0.1.0
7+
description: >
8+
SonataFlow workflow for agent lifecycle approval. When an agent is submitted
9+
for review (draft -> review), this workflow suspends in a callback state
10+
awaiting an admin decision CloudEvent. If no decision is received within
11+
the configured timeout, an escalation notification is sent.
12+
timeouts:
13+
workflowExecTimeout:
14+
duration: P7D
15+
start: NotifyReviewSubmitted
16+
extensions:
17+
- extensionid: workflow-uri-definitions
18+
definitions:
19+
notifications: 'https://raw.githubusercontent.com/rhdhorchestrator/serverless-workflows/main/workflows/shared/specs/notifications-openapi.yaml'
20+
augment: 'specs/augment-agent-lifecycle.yaml'
21+
dataInputSchema:
22+
failOnValidationErrors: true
23+
schema: schemas/agent-approval-input.json
24+
errors:
25+
- name: approvalTimeout
26+
code: TimedOut
27+
- name: notAvailable
28+
code: '404'
29+
functions:
30+
- name: createNotification
31+
operation: notifications#createNotification
32+
- name: promoteAgent
33+
operation: augment#promoteAgent
34+
- name: demoteAgent
35+
operation: augment#demoteAgent
36+
- name: logInfo
37+
type: custom
38+
operation: 'sysout:INFO'
39+
events:
40+
- name: approvalDecisionEvent
41+
source: augment.admin
42+
type: io.rhdhorchestrator.agent.approval.decision
43+
correlation:
44+
- contextAttributeName: kogitoprocrefid
45+
states:
46+
- name: NotifyReviewSubmitted
47+
type: operation
48+
actions:
49+
- name: logSubmission
50+
functionRef:
51+
refName: logInfo
52+
arguments:
53+
message: '"Agent submitted for review: " + .agentId + " by " + .submittedBy'
54+
- name: notifyAdmins
55+
functionRef:
56+
refName: createNotification
57+
arguments:
58+
recipients:
59+
type: 'broadcast'
60+
payload:
61+
title: '"Agent Review: " + .agentName + " (" + .agentId + ")"'
62+
description: '"Agent submitted for review by " + .submittedBy + ". Please approve or reject."'
63+
topic: 'Agent Lifecycle'
64+
link: .reviewUrl
65+
severity: 'normal'
66+
onErrors:
67+
- errorRef: notAvailable
68+
transition: WaitForDecision
69+
transition: WaitForDecision
70+
71+
- name: WaitForDecision
72+
type: callback
73+
action:
74+
functionRef:
75+
refName: logInfo
76+
arguments:
77+
message: '"Waiting for admin decision on agent: " + .agentId'
78+
eventRef: approvalDecisionEvent
79+
eventDataFilter:
80+
data: '.decision'
81+
toStateData: '.adminDecision'
82+
timeouts:
83+
eventTimeout: PT72H
84+
onErrors:
85+
- errorRef: approvalTimeout
86+
transition: EscalateTimeout
87+
transition: ProcessDecision
88+
89+
- name: EscalateTimeout
90+
type: operation
91+
actions:
92+
- name: logEscalation
93+
functionRef:
94+
refName: logInfo
95+
arguments:
96+
message: '"Escalation: No decision on agent " + .agentId + " within 72h"'
97+
- name: escalationNotification
98+
functionRef:
99+
refName: createNotification
100+
arguments:
101+
recipients:
102+
type: 'broadcast'
103+
payload:
104+
title: '"ESCALATION: Agent " + .agentName + " awaiting review for 72+ hours"'
105+
description: '"Agent " + .agentId + " submitted by " + .submittedBy + " has been pending review for over 72 hours. Please take action."'
106+
topic: 'Agent Lifecycle'
107+
link: .reviewUrl
108+
severity: 'high'
109+
onErrors:
110+
- errorRef: notAvailable
111+
transition: WaitForDecision
112+
transition: WaitForDecision
113+
114+
- name: ProcessDecision
115+
type: switch
116+
dataConditions:
117+
- condition: (.adminDecision.approved == true)
118+
transition: ApproveAgent
119+
- condition: (.adminDecision.approved == false)
120+
transition: RejectAgent
121+
defaultCondition:
122+
transition: WaitForDecision
123+
124+
- name: ApproveAgent
125+
type: operation
126+
actions:
127+
- name: promoteToStaging
128+
functionRef:
129+
refName: promoteAgent
130+
arguments:
131+
agentId: .agentId
132+
targetStage: 'staging'
133+
- name: notifyApproval
134+
functionRef:
135+
refName: createNotification
136+
arguments:
137+
recipients:
138+
entityRef: .submittedBy
139+
payload:
140+
title: '"Agent Approved: " + .agentName'
141+
description: '"Your agent " + .agentId + " has been approved and moved to staging by " + .adminDecision.decidedBy'
142+
topic: 'Agent Lifecycle'
143+
severity: 'normal'
144+
stateDataFilter:
145+
output: '{agentId: .agentId, outcome: "approved", decidedBy: .adminDecision.decidedBy}'
146+
end: true
147+
148+
- name: RejectAgent
149+
type: operation
150+
actions:
151+
- name: demoteToDraft
152+
functionRef:
153+
refName: demoteAgent
154+
arguments:
155+
agentId: .agentId
156+
targetStage: 'draft'
157+
reason: .adminDecision.reason
158+
- name: notifyRejection
159+
functionRef:
160+
refName: createNotification
161+
arguments:
162+
recipients:
163+
entityRef: .submittedBy
164+
payload:
165+
title: '"Agent Rejected: " + .agentName'
166+
description: '"Your agent " + .agentId + " was rejected by " + .adminDecision.decidedBy + ". Reason: " + (.adminDecision.reason // "No reason provided")'
167+
topic: 'Agent Lifecycle'
168+
severity: 'normal'
169+
stateDataFilter:
170+
output: '{agentId: .agentId, outcome: "rejected", decidedBy: .adminDecision.decidedBy, reason: .adminDecision.reason}'
171+
end: true
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Agent Approval Workflow - SonataFlow Configuration
2+
# Deploy on OpenShift with the SonataFlow Operator
3+
4+
# Workflow ID
5+
quarkus.rest-client.notifications.url=${NOTIFICATIONS_URL:http://backstage-backend.backstage.svc.cluster.local:7007/api/notifications}
6+
quarkus.rest-client.augment.url=${AUGMENT_BACKEND_URL:http://backstage-backend.backstage.svc.cluster.local:7007/api/augment}
7+
8+
# Knative Eventing - CloudEvent sink for emitting events
9+
mp.messaging.outgoing.kogito_outgoing_stream.url=${K_SINK:http://broker-ingress.knative-eventing.svc.cluster.local/default/default}
10+
11+
# OIDC / Service Account authentication for backend calls
12+
quarkus.oidc-client.client-id=${OIDC_CLIENT_ID:sonataflow-agent-approval}
13+
quarkus.oidc-client.client-secret=${OIDC_CLIENT_SECRET:}
14+
quarkus.oidc-client.token-path=${OIDC_TOKEN_PATH:/realms/backstage/protocol/openid-connect/token}
15+
quarkus.oidc-client.auth-server-url=${OIDC_AUTH_SERVER_URL:}
16+
17+
# Correlation: CloudEvents must include kogitoprocrefid for instance matching
18+
kogito.addon.events.process.kogitoprocrefid-enabled=true
19+
20+
# Approval timeout (overridable via secret)
21+
# Default: 72 hours before escalation
22+
agent.approval.timeout=PT72H
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Agent Approval Workflow Input",
4+
"description": "Input data for the agent lifecycle approval workflow",
5+
"type": "object",
6+
"required": ["agentId", "agentName", "submittedBy"],
7+
"properties": {
8+
"agentId": {
9+
"type": "string",
10+
"description": "Unique agent identifier (e.g. 'namespace/name' or agent key)"
11+
},
12+
"agentName": {
13+
"type": "string",
14+
"description": "Human-readable agent display name"
15+
},
16+
"submittedBy": {
17+
"type": "string",
18+
"description": "Backstage user entity ref of who submitted the agent for review"
19+
},
20+
"reviewUrl": {
21+
"type": "string",
22+
"description": "URL to the review queue in RHDH for deep-linking in notifications"
23+
},
24+
"agentSource": {
25+
"type": "string",
26+
"enum": ["kagenti", "orchestration", "workflow-builder"],
27+
"description": "Origin of the agent"
28+
},
29+
"agentFramework": {
30+
"type": "string",
31+
"description": "Agent framework (e.g. 'a2a', 'responses-api', 'workflow-builder')"
32+
}
33+
},
34+
"additionalProperties": false
35+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Augment Agent Lifecycle API
4+
description: >
5+
Subset of the RHDH Augment plugin backend API used by the SonataFlow
6+
agent-approval workflow. Only the promote and demote endpoints are
7+
exposed here for workflow automation.
8+
version: 1.0.0
9+
servers:
10+
- url: '{augmentBackendUrl}'
11+
description: RHDH Augment backend (injected at deploy time)
12+
variables:
13+
augmentBackendUrl:
14+
default: http://backstage-backend.backstage.svc.cluster.local:7007/api/augment
15+
paths:
16+
/agents/{agentId}/promote:
17+
put:
18+
operationId: promoteAgent
19+
summary: Promote an agent to the next lifecycle stage
20+
parameters:
21+
- name: agentId
22+
in: path
23+
required: true
24+
schema:
25+
type: string
26+
requestBody:
27+
required: true
28+
content:
29+
application/json:
30+
schema:
31+
type: object
32+
properties:
33+
targetStage:
34+
type: string
35+
enum: [draft, review, staging, production, retired]
36+
responses:
37+
'200':
38+
description: Agent promoted successfully
39+
content:
40+
application/json:
41+
schema:
42+
type: object
43+
properties:
44+
success:
45+
type: boolean
46+
agentId:
47+
type: string
48+
lifecycleStage:
49+
type: string
50+
version:
51+
type: integer
52+
/agents/{agentId}/demote:
53+
put:
54+
operationId: demoteAgent
55+
summary: Demote an agent to a previous lifecycle stage
56+
parameters:
57+
- name: agentId
58+
in: path
59+
required: true
60+
schema:
61+
type: string
62+
requestBody:
63+
required: true
64+
content:
65+
application/json:
66+
schema:
67+
type: object
68+
properties:
69+
targetStage:
70+
type: string
71+
enum: [draft, review, staging, production, retired]
72+
reason:
73+
type: string
74+
description: Rejection reason (for review -> draft transitions)
75+
responses:
76+
'200':
77+
description: Agent demoted successfully
78+
content:
79+
application/json:
80+
schema:
81+
type: object
82+
properties:
83+
success:
84+
type: boolean
85+
agentId:
86+
type: string
87+
lifecycleStage:
88+
type: string

0 commit comments

Comments
 (0)