botserver/src/designer/workflow_canvas.rs
Rodrigo Rodriguez e143968179 feat: Add JWT secret rotation and health verification
SEC-02: Implement credential rotation security improvements

- Add JWT secret rotation to rotate-secret command
- Generate 64-character HS512-compatible secrets
- Automatic .env backup with timestamp
- Atomic file updates via temp+rename pattern
- Add health verification for rotated credentials
- Route rotate-secret, rotate-secrets, vault commands in CLI
- Add verification attempts for database and JWT endpoints

Security improvements:
- JWT_SECRET now rotatable (previously impossible)
- Automatic rollback via backup files
- Health checks catch configuration errors
- Clear warnings about token invalidation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 19:42:41 +00:00

420 lines
14 KiB
Rust

use crate::core::shared::state::AppState;
use crate::designer::bas_analyzer::{BasFileAnalyzer, BasFileType, WorkflowMetadata};
use axum::{
extract::State,
http::StatusCode,
response::Html,
Json,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowNode {
pub id: String,
pub node_type: NodeType,
pub position: Position,
pub config: NodeConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Position {
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum NodeType {
BotAgent { bot_name: String, action: String },
HumanApproval { approver: String, timeout: u32 },
Condition { expression: String },
Parallel { branches: Vec<String> },
Event { event_name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
pub label: String,
pub description: Option<String>,
pub parameters: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowConnection {
pub from_node: String,
pub to_node: String,
pub condition: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowCanvas {
pub id: Uuid,
pub name: String,
pub nodes: Vec<WorkflowNode>,
pub connections: Vec<WorkflowConnection>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl WorkflowCanvas {
pub fn new(name: String) -> Self {
Self {
id: Uuid::new_v4(),
name,
nodes: Vec::new(),
connections: Vec::new(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
}
}
pub fn generate_basic_code(&self) -> String {
let mut code = format!("' Generated workflow: {}\n", self.name);
code.push_str(&format!("ORCHESTRATE WORKFLOW \"{}\"\n", self.name));
let mut step_counter = 1;
for node in &self.nodes {
match &node.node_type {
NodeType::BotAgent { bot_name, action } => {
code.push_str(&format!(" STEP {}: BOT \"{}\" \"{}\"\n", step_counter, bot_name, action));
step_counter += 1;
}
NodeType::HumanApproval { approver, timeout } => {
code.push_str(&format!(" STEP {}: HUMAN APPROVAL FROM \"{}\"\n", step_counter, approver));
code.push_str(&format!(" TIMEOUT {}\n", timeout));
step_counter += 1;
}
NodeType::Condition { expression } => {
code.push_str(&format!(" IF {} THEN\n", expression));
}
NodeType::Parallel { branches: _ } => {
code.push_str(&format!(" STEP {}: PARALLEL\n", step_counter));
code.push_str(" BRANCH A: BOT \"branch-a\" \"process\"\n");
code.push_str(" BRANCH B: BOT \"branch-b\" \"process\"\n");
code.push_str(" END PARALLEL\n");
step_counter += 1;
}
NodeType::Event { event_name } => {
code.push_str(&format!(" PUBLISH EVENT \"{}\"\n", event_name));
}
}
}
code.push_str("END WORKFLOW\n");
code
}
pub fn detect_file_type(content: &str) -> BasFileType {
match BasFileAnalyzer::analyze_content(content) {
Ok(file_type) => file_type,
Err(_) => BasFileType::Regular,
}
}
pub fn get_metadata(&self) -> WorkflowMetadata {
let code = self.generate_basic_code();
BasFileAnalyzer::get_workflow_metadata(&code)
}
}
pub async fn workflow_designer_page(
State(_state): State<Arc<AppState>>,
) -> Result<Html<String>, StatusCode> {
let html = r##"
<!DOCTYPE html>
<html>
<head>
<title>Workflow Designer</title>
<script src="/static/htmx.min.js"></script>
<style>
.canvas {
width: 100%;
height: 600px;
border: 1px solid #ccc;
position: relative;
background: #f9f9f9;
}
.node {
position: absolute;
padding: 10px;
border: 2px solid #333;
background: white;
border-radius: 5px;
cursor: move;
min-width: 120px;
text-align: center;
}
.node.bot-agent { border-color: #007bff; background: #e7f3ff; }
.node.human-approval { border-color: #ffc107; background: #fff8e1; }
.node.condition { border-color: #28a745; background: #e8f5e9; }
.node.parallel { border-color: #6f42c1; background: #f3e5f5; }
.node.event { border-color: #fd7e14; background: #fff3e0; }
.toolbar {
padding: 10px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.btn {
padding: 8px 16px;
margin: 0 5px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-warning { background: #ffc107; color: black; }
.code-preview {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #dee2e6;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<div class="toolbar">
<h2>Workflow Designer</h2>
<button class="btn btn-primary" onclick="addNode('bot-agent')">Add Bot</button>
<button class="btn btn-warning" onclick="addNode('human-approval')">Add Approval</button>
<button class="btn btn-success" onclick="addNode('condition')">Add Condition</button>
<button class="btn btn-primary" onclick="addNode('parallel')">Add Parallel</button>
<button class="btn" onclick="addNode('event')">Add Event</button>
<button class="btn btn-success" hx-post="/api/workflow/generate" hx-target="#code-preview">Generate Code</button>
<input type="file" id="file-input" accept=".bas" onchange="analyzeFile()" style="margin-left: 20px;">
<label for="file-input" class="btn">Analyze .bas File</label>
</div>
<div id="file-analysis" style="display:none; padding: 10px; background: #e8f4f8; border: 1px solid #bee5eb; margin: 10px 0;">
<h4>File Analysis Result</h4>
<div id="analysis-content"></div>
</div>
<div id="canvas" class="canvas" ondrop="drop(event)" ondragover="allowDrop(event)">
</div>
<div id="code-preview" class="code-preview">
Generated BASIC code will appear here...
</div>
<script>
let nodeCounter = 0;
let nodes = [];
function addNode(type) {
nodeCounter++;
const node = {
id: 'node-' + nodeCounter,
type: type,
x: 50 + (nodeCounter * 20),
y: 50 + (nodeCounter * 20)
};
nodes.push(node);
renderNode(node);
}
function renderNode(node) {
const canvas = document.getElementById('canvas');
const nodeEl = document.createElement('div');
nodeEl.className = 'node ' + node.type;
nodeEl.id = node.id;
nodeEl.draggable = true;
nodeEl.style.left = node.x + 'px';
nodeEl.style.top = node.y + 'px';
let content = '';
switch(node.type) {
case 'bot-agent':
content = '<strong>Bot Agent</strong><br><input type="text" placeholder="Bot Name " style="width:100px;margin:2px;"><br><input type="text" placeholder="Action" style="width:100px;margin:2px;">';
break;
case 'human-approval':
content = '<strong>Human Approval</strong><br><input type="text" placeholder="Approver" style="width:100px;margin:2px;"><br><input type="number" placeholder="Timeout" style="width:100px;margin:2px;">';
break;
case 'condition':
content = '<strong>Condition</strong><br><input type="text" placeholder="Expression" style="width:100px;margin:2px;">';
break;
case 'parallel':
content = '<strong>Parallel</strong><br>Multiple branches';
break;
case 'event':
content = '<strong>Event</strong><br><input type="text" placeholder="Event Name " style="width:100px;margin:2px;">';
break;
}
nodeEl.innerHTML = content;
nodeEl.ondragstart = drag;
canvas.appendChild(nodeEl);
}
function allowDrop(ev) {
ev.preventDefault();
}
function drag(ev) {
ev.dataTransfer.setData("text", ev.target.id);
}
function drop(ev) {
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const nodeEl = document.getElementById(data);
const rect = ev.currentTarget.getBoundingClientRect();
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
nodeEl.style.left = x + 'px';
nodeEl.style.top = y + 'px';
// Update node position in data
const node = nodes.find(n => n.id === data);
if (node) {
node.x = x;
node.y = y;
}
}
function analyzeFile() {
const fileInput = document.getElementById('file-input');
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
fetch('/api/workflow/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: content })
})
.then(response => response.json())
.then(data => {
displayAnalysis(data);
})
.catch(error => {
console.error('Analysis failed:', error);
});
};
reader.readAsText(file);
}
}
function displayAnalysis(analysis) {
const analysisDiv = document.getElementById('file-analysis');
const contentDiv = document.getElementById('analysis-content');
let html = `<p><strong>File Type:</strong> ${analysis.file_type}</p>`;
if (analysis.metadata) {
html += `<p><strong>Workflow Name:</strong> ${analysis.metadata.name}</p>`;
html += `<p><strong>Steps:</strong> ${analysis.metadata.step_count}</p>`;
html += `<p><strong>Bots Used:</strong> ${analysis.metadata.bot_count}</p>`;
html += `<p><strong>Human Approval:</strong> ${analysis.metadata.has_human_approval ? 'Yes' : 'No'}</p>`;
html += `<p><strong>Parallel Processing:</strong> ${analysis.metadata.has_parallel ? 'Yes' : 'No'}</p>`;
}
if (analysis.suggestions.length > 0) {
html += '<p><strong>Suggestions:</strong></p><ul>';
analysis.suggestions.forEach(suggestion => {
html += `<li>${suggestion}</li>`;
});
html += '</ul>';
}
contentDiv.innerHTML = html;
analysisDiv.style.display = 'block';
}
</script>
</body>
</html>
"##;
Ok(Html(html.to_string()))
}
#[derive(Deserialize)]
pub struct GenerateCodeRequest {
pub nodes: Vec<WorkflowNode>,
pub connections: Vec<WorkflowConnection>,
}
pub async fn generate_workflow_code(
State(_state): State<Arc<AppState>>,
Json(request): Json<GenerateCodeRequest>,
) -> Result<Html<String>, StatusCode> {
let canvas = WorkflowCanvas {
id: Uuid::new_v4(),
name: "Generated Workflow".to_string(),
nodes: request.nodes,
connections: request.connections,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let code = canvas.generate_basic_code();
Ok(Html(format!("<pre>{}</pre>", code)))
}
#[derive(Deserialize)]
pub struct AnalyzeFileRequest {
pub content: String,
}
#[derive(Serialize)]
pub struct AnalyzeFileResponse {
pub file_type: String,
pub metadata: Option<WorkflowMetadata>,
pub suggestions: Vec<String>,
}
pub async fn analyze_bas_file(
State(_state): State<Arc<AppState>>,
Json(request): Json<AnalyzeFileRequest>,
) -> Result<Json<AnalyzeFileResponse>, StatusCode> {
let file_type = WorkflowCanvas::detect_file_type(&request.content);
let (type_str, metadata, suggestions) = match file_type {
BasFileType::Workflow => {
let meta = BasFileAnalyzer::get_workflow_metadata(&request.content);
let mut suggestions = Vec::new();
if meta.step_count > 10 {
suggestions.push("Consider breaking this workflow into smaller sub-workflows".to_string());
}
if meta.bot_count > 5 {
suggestions.push("High bot count - ensure proper resource management".to_string());
}
if !meta.has_human_approval && meta.step_count > 3 {
suggestions.push("Consider adding human approval for complex workflows".to_string());
}
("workflow".to_string(), Some(meta), suggestions)
}
BasFileType::Tool => {
let suggestions = vec![
"Tools should be simple and focused on single operations".to_string(),
"Consider using USE TOOL instead of complex logic".to_string(),
];
("tool".to_string(), None, suggestions)
}
BasFileType::Regular => {
let suggestions = vec![
"Regular bot - consider upgrading to workflow for complex logic".to_string(),
];
("regular".to_string(), None, suggestions)
}
};
Ok(Json(AnalyzeFileResponse {
file_type: type_str,
metadata,
suggestions,
}))
}