Files
heicode-win/heicode/controller/agnet_control_plane.go
T
gongzhiyong 1f21309597 refactor: rename manager codebase dir new-api → heicode, module github.com/heicode/manager
Remove user-facing new-api naming; Docker/network/container names use heicode.
Go imports updated; Dockerfiles and workflows ldflags fixed.

Made-with: Cursor
2026-05-01 01:47:23 +08:00

478 lines
13 KiB
Go

package controller
import (
"net/http"
"strings"
"sync"
"time"
"github.com/heicode/manager/common"
"github.com/gin-gonic/gin"
)
const (
agnetRiskLow = "low"
agnetRiskMedium = "medium"
agnetRiskHigh = "high"
)
type agnetBudget struct {
MaxTokens int `json:"max_tokens"`
MaxCostUSD float64 `json:"max_cost_usd"`
MaxDurationSec int `json:"max_duration_sec"`
}
type agnetSKSource struct {
Type string `json:"type"`
ArtifactID string `json:"artifact_id"`
}
type agnetAgentPlan struct {
RoleTemplate string `json:"role_template"`
Goal string `json:"goal"`
DefaultModelID string `json:"default_model_id"`
SKSources []agnetSKSource `json:"sk_sources"`
}
type agnetConstraints struct {
AllowedModelIDs []string `json:"allowed_model_ids"`
}
type agnetMetadata struct {
TenantID string `json:"tenant_id"`
ProjectID string `json:"project_id"`
CorrelationID string `json:"correlation_id"`
}
type agnetOrchestrationPlan struct {
IntentID string `json:"intent_id"`
TemplateHint string `json:"template_hint"`
Objective string `json:"objective"`
RiskLevel string `json:"risk_level"`
Budget agnetBudget `json:"budget"`
Agents []agnetAgentPlan `json:"agents"`
Constraints agnetConstraints `json:"constraints"`
Metadata agnetMetadata `json:"metadata"`
}
type agnetDeploymentRequest struct {
Plan agnetOrchestrationPlan `json:"orchestration_plan"`
}
type agnetDeploymentRecord struct {
DeploymentID string `json:"deployment_id"`
Status string `json:"status"`
Phase string `json:"phase"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Plan agnetOrchestrationPlan `json:"orchestration_plan"`
}
type agnetEvent struct {
EventID string `json:"event_id"`
Event string `json:"event"`
SchemaVersion int `json:"schema_version"`
TenantID string `json:"tenant_id"`
ProjectID string `json:"project_id"`
DeploymentID string `json:"deployment_id"`
CorrelationID string `json:"correlation_id"`
OccurredAt string `json:"occurred_at"`
}
type agnetSKSnapshotResolveRequest struct {
DeploymentID string `json:"deployment_id"`
}
type agnetSKSnapshot struct {
SnapshotID string `json:"snapshot_id"`
DeploymentID string `json:"deployment_id"`
TenantID string `json:"tenant_id"`
ProjectID string `json:"project_id"`
SourceType string `json:"source_type"`
SourceRef string `json:"source_ref"`
ResolvedAt string `json:"resolved_at"`
}
var (
agnetMu sync.RWMutex
agnetDeployments = make(map[string]agnetDeploymentRecord)
agnetEvents = make(map[string][]agnetEvent)
agnetSnapshots = make(map[string][]agnetSKSnapshot)
)
func agnetNow() string {
return time.Now().UTC().Format(time.RFC3339)
}
func agnetRequestID(c *gin.Context) string {
if reqID := strings.TrimSpace(c.GetString(common.RequestIdKey)); reqID != "" {
return reqID
}
return common.GetUUID()
}
func agnetError(c *gin.Context, code string, message string) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": message,
"error": gin.H{
"code": code,
"message": message,
"request_id": agnetRequestID(c),
},
})
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func validateOrchestrationPlan(c *gin.Context, plan agnetOrchestrationPlan) bool {
if strings.TrimSpace(plan.IntentID) == "" ||
strings.TrimSpace(plan.TemplateHint) == "" ||
strings.TrimSpace(plan.Objective) == "" {
agnetError(c, "POLICY_REJECTED", "intent_id/template_hint/objective is required")
return false
}
if len(plan.Agents) == 0 {
agnetError(c, "POLICY_REJECTED", "at least one agent is required")
return false
}
if strings.TrimSpace(plan.Metadata.TenantID) == "" ||
strings.TrimSpace(plan.Metadata.ProjectID) == "" ||
strings.TrimSpace(plan.Metadata.CorrelationID) == "" {
agnetError(c, "POLICY_REJECTED", "metadata.tenant_id/project_id/correlation_id is required")
return false
}
switch plan.RiskLevel {
case agnetRiskLow, agnetRiskMedium, agnetRiskHigh:
default:
agnetError(c, "POLICY_REJECTED", "risk_level must be low/medium/high")
return false
}
if plan.Budget.MaxTokens <= 0 || plan.Budget.MaxCostUSD <= 0 || plan.Budget.MaxDurationSec <= 0 {
agnetError(c, "POLICY_REJECTED", "budget.max_tokens/max_cost_usd/max_duration_sec must be positive")
return false
}
if plan.Budget.MaxTokens > 500000 || plan.Budget.MaxCostUSD > 200 || plan.Budget.MaxDurationSec > 24*3600 {
agnetError(c, "BUDGET_EXCEEDED", "budget exceeds current platform policy limits")
return false
}
if tenantHeader := strings.TrimSpace(c.GetHeader("X-Tenant-Id")); tenantHeader != "" && tenantHeader != plan.Metadata.TenantID {
agnetError(c, "FORBIDDEN_CROSS_TENANT", "X-Tenant-Id does not match orchestration_plan metadata.tenant_id")
return false
}
allowedModels := plan.Constraints.AllowedModelIDs
for _, agent := range plan.Agents {
if strings.TrimSpace(agent.RoleTemplate) == "" || strings.TrimSpace(agent.Goal) == "" {
agnetError(c, "POLICY_REJECTED", "each agent must contain role_template and goal")
return false
}
for _, source := range agent.SKSources {
sourceType := strings.TrimSpace(source.Type)
if sourceType != "" && sourceType != "git" && sourceType != "upload" {
agnetError(c, "SK_SOURCE_UNRESOLVABLE", "unsupported sk source type")
return false
}
}
modelID := strings.TrimSpace(agent.DefaultModelID)
if modelID != "" && len(allowedModels) > 0 && !containsString(allowedModels, modelID) {
agnetError(c, "MODEL_NOT_ALLOWED", "agent default_model_id is outside allowed_model_ids")
return false
}
}
return true
}
func AgnetCreateDeployment(c *gin.Context) {
var req agnetDeploymentRequest
if err := c.ShouldBindJSON(&req); err != nil {
agnetError(c, "POLICY_REJECTED", err.Error())
return
}
plan := req.Plan
if !validateOrchestrationPlan(c, plan) {
return
}
now := agnetNow()
deploymentID := "dep_" + common.GetUUID()[:12]
record := agnetDeploymentRecord{
DeploymentID: deploymentID,
Status: "accepted",
Phase: "pending",
CreatedAt: now,
UpdatedAt: now,
Plan: plan,
}
event := agnetEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "deployment.accepted",
SchemaVersion: 1,
TenantID: plan.Metadata.TenantID,
ProjectID: plan.Metadata.ProjectID,
DeploymentID: deploymentID,
CorrelationID: plan.Metadata.CorrelationID,
OccurredAt: now,
}
agnetMu.Lock()
agnetDeployments[deploymentID] = record
agnetEvents[deploymentID] = append(agnetEvents[deploymentID], event)
agnetMu.Unlock()
common.ApiSuccess(c, gin.H{
"deployment_id": deploymentID,
"status": "accepted",
"agent_instances": []gin.H{
{
"instance_id": "agi_" + common.GetUUID()[:12],
"role": plan.Agents[0].RoleTemplate,
"phase": "pending",
},
},
})
}
func AgnetGetDeployment(c *gin.Context) {
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
if deploymentID == "" {
agnetError(c, "POLICY_REJECTED", "deployment_id is required")
return
}
agnetMu.RLock()
record, ok := agnetDeployments[deploymentID]
agnetMu.RUnlock()
if !ok {
agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found")
return
}
common.ApiSuccess(c, record)
}
func AgnetListDeployments(c *gin.Context) {
tenantID := strings.TrimSpace(c.Query("tenant_id"))
projectID := strings.TrimSpace(c.Query("project_id"))
items := make([]agnetDeploymentRecord, 0)
agnetMu.RLock()
for _, record := range agnetDeployments {
if tenantID != "" && record.Plan.Metadata.TenantID != tenantID {
continue
}
if projectID != "" && record.Plan.Metadata.ProjectID != projectID {
continue
}
items = append(items, record)
}
agnetMu.RUnlock()
common.ApiSuccess(c, gin.H{
"items": items,
"total": len(items),
})
}
func AgnetStopDeployment(c *gin.Context) {
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
if deploymentID == "" {
agnetError(c, "POLICY_REJECTED", "deployment_id is required")
return
}
agnetMu.Lock()
record, ok := agnetDeployments[deploymentID]
if !ok {
agnetMu.Unlock()
agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found")
return
}
record.Status = "stopped"
record.Phase = "stopped"
record.UpdatedAt = agnetNow()
agnetDeployments[deploymentID] = record
agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "instance.phase_changed",
SchemaVersion: 1,
TenantID: record.Plan.Metadata.TenantID,
ProjectID: record.Plan.Metadata.ProjectID,
DeploymentID: deploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agnetNow(),
})
agnetMu.Unlock()
common.ApiSuccess(c, gin.H{
"deployment_id": deploymentID,
"status": "stopped",
})
}
func AgnetListDeploymentEvents(c *gin.Context) {
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
if deploymentID == "" {
agnetError(c, "POLICY_REJECTED", "deployment_id is required")
return
}
agnetMu.RLock()
events := agnetEvents[deploymentID]
agnetMu.RUnlock()
common.ApiSuccess(c, gin.H{
"items": events,
"total": len(events),
})
}
func AgnetProjectDashboardSnapshot(c *gin.Context) {
projectID := strings.TrimSpace(c.Param("project_id"))
if projectID == "" {
agnetError(c, "POLICY_REJECTED", "project_id is required")
return
}
active := 0
pending := 0
stopped := 0
agnetMu.RLock()
for _, record := range agnetDeployments {
if record.Plan.Metadata.ProjectID != projectID {
continue
}
if record.Status == "accepted" {
active++
}
if record.Phase == "pending" {
pending++
}
if record.Phase == "stopped" {
stopped++
}
}
agnetMu.RUnlock()
common.ApiSuccess(c, gin.H{
"project_id": projectID,
"active_instances": active,
"phase_distribution": gin.H{"pending": pending, "stopped": stopped},
"failure_rate_1h": 0,
"avg_task_duration": 0,
})
}
func AgnetResolveSKSnapshots(c *gin.Context) {
var req agnetSKSnapshotResolveRequest
if err := c.ShouldBindJSON(&req); err != nil {
agnetError(c, "SK_SOURCE_UNRESOLVABLE", err.Error())
return
}
deploymentID := strings.TrimSpace(req.DeploymentID)
if deploymentID == "" {
agnetError(c, "SK_SOURCE_UNRESOLVABLE", "deployment_id is required")
return
}
agnetMu.Lock()
record, ok := agnetDeployments[deploymentID]
if !ok {
agnetMu.Unlock()
agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found")
return
}
snapshots := make([]agnetSKSnapshot, 0)
now := agnetNow()
for _, agent := range record.Plan.Agents {
for _, source := range agent.SKSources {
sourceType := strings.TrimSpace(source.Type)
if sourceType == "" {
continue
}
sourceRef := strings.TrimSpace(source.ArtifactID)
if sourceRef == "" {
sourceRef = "ref_" + common.GetUUID()[:8]
}
snapshots = append(snapshots, agnetSKSnapshot{
SnapshotID: "sks_" + common.GetUUID()[:12],
DeploymentID: deploymentID,
TenantID: record.Plan.Metadata.TenantID,
ProjectID: record.Plan.Metadata.ProjectID,
SourceType: sourceType,
SourceRef: sourceRef,
ResolvedAt: now,
})
}
}
agnetSnapshots[deploymentID] = snapshots
agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "sk_snapshot_refreshed",
SchemaVersion: 1,
TenantID: record.Plan.Metadata.TenantID,
ProjectID: record.Plan.Metadata.ProjectID,
DeploymentID: deploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: now,
})
agnetMu.Unlock()
common.ApiSuccess(c, gin.H{
"deployment_id": deploymentID,
"items": snapshots,
"total": len(snapshots),
})
}
func AgnetListSKSnapshots(c *gin.Context) {
deploymentID := strings.TrimSpace(c.Param("deployment_id"))
if deploymentID == "" {
agnetError(c, "SK_SOURCE_UNRESOLVABLE", "deployment_id is required")
return
}
agnetMu.RLock()
items := agnetSnapshots[deploymentID]
agnetMu.RUnlock()
common.ApiSuccess(c, gin.H{
"deployment_id": deploymentID,
"items": items,
"total": len(items),
})
}
func AgnetListAuditLogs(c *gin.Context) {
projectID := strings.TrimSpace(c.Query("project_id"))
items := make([]gin.H, 0)
agnetMu.RLock()
for deploymentID, events := range agnetEvents {
for _, event := range events {
if projectID != "" && event.ProjectID != projectID {
continue
}
items = append(items, gin.H{
"actor": "agnet_control_plane",
"action": event.Event,
"resource": deploymentID,
"tenant_id": event.TenantID,
"request_id": agnetRequestID(c),
"correlation_id": event.CorrelationID,
"result": "ok",
"occurred_at": event.OccurredAt,
})
}
}
agnetMu.RUnlock()
common.ApiSuccess(c, gin.H{
"items": items,
"total": len(items),
})
}