package controller import ( "fmt" "net/http" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/heicode/manager/common" ) const ( agnetRiskLow = "low" agnetRiskMedium = "medium" agnetRiskHigh = "high" agnetResourceGit = "git" agnetResourceSK = "sk" agnetResourceProjectDoc = "project_doc" agnetResourceCloudAccount = "cloud_account" agnetResourceCloudResource = "cloud_resource" agnetGrantStatusPending = "pending" agnetGrantStatusActive = "active" agnetGrantStatusDisabled = "disabled" agnetGrantStatusRevoked = "revoked" ) type agnetBudget struct { MaxTokens int `json:"max_tokens"` MaxCostUSD float64 `json:"max_cost_usd"` MaxDurationSec int `json:"max_duration_sec"` } type agnetUserContext struct { UserID string `json:"user_id"` Email string `json:"email"` Role string `json:"role"` ChannelID string `json:"channel_id"` SubscriptionTier string `json:"subscription_tier"` } type agnetBillingContext struct { Provider string `json:"provider"` NewAPIUserRef string `json:"newapi_user_ref"` NewAPIGroup string `json:"newapi_group"` QuotaRef string `json:"quota_ref"` } type agnetRuntimeAgent struct { Role string `json:"role"` ModelRef string `json:"model_ref"` InstanceCount int `json:"instance_count"` } type agnetAgentRuntime struct { Platform string `json:"platform"` Agents []agnetRuntimeAgent `json:"agents"` } // agnetRepoRef matches docs/integration/orchestration-plan-contract.md (git sk_sources). type agnetRepoRef struct { ConnectionID string `json:"connection_id"` RepoURL string `json:"repo_url"` Ref string `json:"ref"` Paths []string `json:"paths"` } type agnetSKSource struct { Type string `json:"type"` ArtifactID string `json:"artifact_id"` Mime string `json:"mime"` RepoRef agnetRepoRef `json:"repo_ref"` } // agnetRuntimeExecution mirrors docs/integration/agnet-platform-api-design.md §5.0 (runtime_execution). type agnetRuntimeExecution struct { ProfileID string `json:"profile_id"` CloudPrincipalRefs []string `json:"cloud_principal_refs"` NetworkPolicyRef string `json:"network_policy_ref"` } // agnetSKAccessPolicy mirrors docs/integration/agnet-platform-api-design.md §5.0 (sk_access_policy). type agnetSKAccessPolicy struct { PolicyRef string `json:"policy_ref"` DenySkillIDs []string `json:"deny_skill_ids"` InheritDeploymentDefaults bool `json:"inherit_deployment_defaults"` } // agnetResourceGrant is the Manager-side resource binding envelope from docs/heicode.md P1. // It deliberately carries only metadata, scoped permissions and secret_ref, never plaintext secrets. type agnetResourceGrant struct { GrantID string `json:"grant_id"` ResourceID string `json:"resource_id"` ResourceType string `json:"resource_type"` UserID string `json:"user_id"` BindingScope string `json:"binding_scope"` TenantID string `json:"tenant_id,omitempty"` // legacy compatibility only. ProjectID string `json:"project_id,omitempty"` // legacy compatibility only. TargetRole string `json:"target_role"` TargetAgentRef string `json:"target_agent_ref"` PermissionScope []string `json:"permission_scope"` Constraints map[string]string `json:"constraints"` Metadata map[string]string `json:"metadata"` Status string `json:"status"` SecretRef string `json:"secret_ref"` Audit map[string]string `json:"audit"` } type agnetAgentPlan struct { RoleTemplate string `json:"role_template"` Goal string `json:"goal"` DefaultModelID string `json:"default_model_id"` SKSources []agnetSKSource `json:"sk_sources"` RuntimeExecution agnetRuntimeExecution `json:"runtime_execution"` SKAccessPolicy agnetSKAccessPolicy `json:"sk_access_policy"` ResourceGrants []agnetResourceGrant `json:"resource_grants"` } type agnetConstraints struct { AllowedModelIDs []string `json:"allowed_model_ids"` } type agnetMetadata struct { TenantID string `json:"tenant_id,omitempty"` // legacy compatibility only. ProjectID string `json:"project_id,omitempty"` // legacy compatibility only. 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"` UserContext agnetUserContext `json:"user_context"` BillingContext agnetBillingContext `json:"billing_context"` AgentRuntime agnetAgentRuntime `json:"agent_runtime"` 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"` RuntimeState string `json:"runtime_state"` FailureReason string `json:"failure_reason"` AgentInstances []agnetAgentInstance `json:"agent_instances"` ResourceGrantManifest agnetPermissionManifest `json:"permission_manifest"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Plan agnetOrchestrationPlan `json:"orchestration_plan"` } type agnetAgentInstance struct { InstanceID string `json:"instance_id"` Role string `json:"role"` Phase string `json:"phase"` RuntimeState string `json:"runtime_state"` FailureReason string `json:"failure_reason"` } type agnetManifestGrant struct { GrantID string `json:"grant_id"` ResourceID string `json:"resource_id"` ResourceType string `json:"resource_type"` ResourceRef string `json:"resource_ref"` AllowedActions []string `json:"allowed_actions"` Constraints map[string]string `json:"constraints"` SecretRef string `json:"secret_ref,omitempty"` Status string `json:"status"` } type agnetPermissionManifest struct { UserID string `json:"user_id"` BindingScope string `json:"binding_scope"` AgentRole string `json:"agent_role"` TargetAgentRef string `json:"target_agent_ref"` ResourceGrants []agnetManifestGrant `json:"resource_grants"` } type agnetEvent struct { EventID string `json:"event_id"` Event string `json:"event"` SchemaVersion int `json:"schema_version"` UserID string `json:"user_id"` ChannelID string `json:"channel_id"` BindingScope string `json:"binding_scope"` 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"` UserID string `json:"user_id"` BindingScope string `json:"binding_scope"` 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 containsSensitiveGrantField(values map[string]string) bool { for key := range values { normalized := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(key), "-", "_")) if strings.Contains(normalized, "password") || strings.Contains(normalized, "token") || strings.Contains(normalized, "secret") || strings.Contains(normalized, "private_key") || strings.Contains(normalized, "access_key") || strings.Contains(normalized, "credential") { return true } } return false } func agnetResourceTypeNeedsSecretRef(resourceType string) bool { switch resourceType { case agnetResourceGit, agnetResourceSK, agnetResourceCloudAccount, agnetResourceCloudResource: return true default: return false } } func agnetRuntimePartiallySet(r agnetRuntimeExecution) bool { return strings.TrimSpace(r.ProfileID) != "" || len(r.CloudPrincipalRefs) > 0 || strings.TrimSpace(r.NetworkPolicyRef) != "" } func validateAgentRuntimeBindings(c *gin.Context, agent agnetAgentPlan) bool { r := agent.RuntimeExecution if !agnetRuntimePartiallySet(r) { return true } if strings.TrimSpace(r.ProfileID) == "" { agnetError(c, "RUNTIME_BINDING_INVALID", "runtime_execution.profile_id is required when runtime bindings are present") return false } return true } func skSourceDisplayRef(s agnetSKSource) string { switch strings.TrimSpace(s.Type) { case "git": r := s.RepoRef ref := strings.TrimSpace(r.Ref) pathPart := strings.Join(r.Paths, ",") if ref != "" && pathPart != "" { return ref + ":" + pathPart } if ref != "" { return ref } return "git" case "upload": id := strings.TrimSpace(s.ArtifactID) if id != "" { return id } return "upload" default: return strings.TrimSpace(s.ArtifactID) } } func validateSKSourceEntry(c *gin.Context, source agnetSKSource) bool { sourceType := strings.TrimSpace(source.Type) if sourceType == "" { return true } switch sourceType { case "git": ref := source.RepoRef if strings.TrimSpace(ref.Ref) == "" { agnetError(c, "SK_SOURCE_UNRESOLVABLE", "git sk_sources.repo_ref.ref is required") return false } if len(ref.Paths) == 0 { agnetError(c, "SK_SOURCE_UNRESOLVABLE", "git sk_sources.repo_ref.paths must not be empty") return false } case "upload": if strings.TrimSpace(source.ArtifactID) == "" { agnetError(c, "SK_SOURCE_UNRESOLVABLE", "upload sk_sources.artifact_id is required") return false } default: agnetError(c, "SK_SOURCE_UNRESOLVABLE", "unsupported sk source type") return false } return true } func validateAgentSKAccessPolicy(c *gin.Context, agent agnetAgentPlan) bool { p := agent.SKAccessPolicy hasDeny := len(p.DenySkillIDs) > 0 if !hasDeny { return true } if strings.TrimSpace(p.PolicyRef) == "" && !p.InheritDeploymentDefaults { agnetError(c, "SK_POLICY_REJECTED", "sk_access_policy.policy_ref or inherit_deployment_defaults is required when deny_skill_ids set") return false } return true } func validateBillingContext(c *gin.Context, plan agnetOrchestrationPlan) bool { billing := plan.BillingContext provider := strings.TrimSpace(strings.ToLower(billing.Provider)) if provider == "" { return true } if provider != "newapi" { agnetError(c, "BILLING_CONTEXT_INVALID", "billing_context.provider must be newapi when set") return false } if strings.TrimSpace(plan.UserContext.ChannelID) == "" && strings.TrimSpace(billing.NewAPIUserRef) == "" && strings.TrimSpace(billing.NewAPIGroup) == "" && strings.TrimSpace(billing.QuotaRef) == "" { agnetError(c, "BILLING_CONTEXT_INVALID", "newapi billing_context requires channel_id, newapi_user_ref, newapi_group, or quota_ref") return false } return true } func validateAgentRuntimeContext(c *gin.Context, plan agnetOrchestrationPlan) bool { runtime := plan.AgentRuntime platform := strings.TrimSpace(strings.ToLower(runtime.Platform)) if platform == "" && len(runtime.Agents) == 0 { return true } if platform != "agnet" { agnetError(c, "AGENT_RUNTIME_INVALID", "agent_runtime.platform must be agnet when runtime context is present") return false } if len(runtime.Agents) == 0 { agnetError(c, "AGENT_RUNTIME_INVALID", "agent_runtime.agents must not be empty when runtime context is present") return false } knownRoles := map[string]bool{} for _, agent := range plan.Agents { role := strings.TrimSpace(agent.RoleTemplate) if role != "" { knownRoles[role] = true } } for _, agent := range runtime.Agents { role := strings.TrimSpace(agent.Role) if role == "" || strings.TrimSpace(agent.ModelRef) == "" || agent.InstanceCount <= 0 { agnetError(c, "AGENT_RUNTIME_INVALID", "agent_runtime agents require role/model_ref/positive instance_count") return false } if len(knownRoles) > 0 && !knownRoles[role] { agnetError(c, "AGENT_RUNTIME_INVALID", "agent_runtime agent role must match an orchestration agent role") return false } } return true } func validateResourceGrant(c *gin.Context, plan agnetOrchestrationPlan, agent agnetAgentPlan, grant agnetResourceGrant) bool { resourceType := strings.TrimSpace(grant.ResourceType) switch resourceType { case agnetResourceGit, agnetResourceSK, agnetResourceProjectDoc, agnetResourceCloudAccount, agnetResourceCloudResource: default: agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.resource_type must be git/sk/project_doc/cloud_account/cloud_resource") return false } if strings.TrimSpace(grant.GrantID) == "" || strings.TrimSpace(grant.ResourceID) == "" || strings.TrimSpace(grant.TargetRole) == "" || strings.TrimSpace(grant.TargetAgentRef) == "" { agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants require grant_id/resource_id/target_role/target_agent_ref") return false } grantUserID := strings.TrimSpace(grant.UserID) planUserID := strings.TrimSpace(plan.UserContext.UserID) bindingScope := strings.TrimSpace(grant.BindingScope) if bindingScope == "" { agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.binding_scope is required") return false } if planUserID != "" && grantUserID != "" && grantUserID != planUserID { agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.user_id must match orchestration user_context.user_id") return false } if strings.TrimSpace(grant.TargetRole) != strings.TrimSpace(agent.RoleTemplate) { agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.target_role must match the assigned agent role") return false } if len(grant.PermissionScope) == 0 { agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.permission_scope must not be empty") return false } switch strings.TrimSpace(grant.Status) { case agnetGrantStatusPending, agnetGrantStatusActive, agnetGrantStatusDisabled, agnetGrantStatusRevoked: default: agnetError(c, "RESOURCE_GRANT_INVALID", "resource_grants.status must be pending/active/disabled/revoked") return false } if agnetResourceTypeNeedsSecretRef(resourceType) && strings.TrimSpace(grant.SecretRef) == "" { agnetError(c, "RESOURCE_GRANT_SECRET_REF_REQUIRED", "resource_grants.secret_ref is required for credential-backed resources") return false } if containsSensitiveGrantField(grant.Metadata) || containsSensitiveGrantField(grant.Constraints) || containsSensitiveGrantField(grant.Audit) { agnetError(c, "RESOURCE_GRANT_SECRET_REJECTED", "resource_grants metadata/constraints/audit must not contain plaintext credential fields") return false } return true } 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.UserContext.UserID) == "" { agnetError(c, "POLICY_REJECTED", "user_context.user_id is required") return false } if strings.TrimSpace(plan.Metadata.CorrelationID) == "" { agnetError(c, "POLICY_REJECTED", "metadata.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 !validateBillingContext(c, plan) { return false } if !validateAgentRuntimeContext(c, plan) { 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 { if !validateSKSourceEntry(c, source) { 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 } if !validateAgentRuntimeBindings(c, agent) { return false } if !validateAgentSKAccessPolicy(c, agent) { return false } for _, grant := range agent.ResourceGrants { if !validateResourceGrant(c, plan, agent, grant) { return false } } } return true } func firstPlanBindingScope(plan agnetOrchestrationPlan) string { for _, agent := range plan.Agents { for _, grant := range agent.ResourceGrants { if scope := strings.TrimSpace(grant.BindingScope); scope != "" { return scope } } } return "" } func agnetGrantResourceRef(grant agnetResourceGrant) string { for _, value := range []string{grant.Metadata["repo_url"], grant.Metadata["doc_ref"], grant.Metadata["resource_ref"], grant.BindingScope, grant.ResourceID} { if strings.TrimSpace(value) != "" { return value } } return grant.GrantID } func buildAgnetPermissionManifest(plan agnetOrchestrationPlan) agnetPermissionManifest { manifest := agnetPermissionManifest{ UserID: plan.UserContext.UserID, BindingScope: firstPlanBindingScope(plan), } for _, agent := range plan.Agents { for _, grant := range agent.ResourceGrants { if strings.TrimSpace(grant.Status) != agnetGrantStatusActive { continue } if manifest.AgentRole == "" { manifest.AgentRole = grant.TargetRole } if manifest.TargetAgentRef == "" { manifest.TargetAgentRef = grant.TargetAgentRef } manifest.ResourceGrants = append(manifest.ResourceGrants, agnetManifestGrant{ GrantID: grant.GrantID, ResourceID: grant.ResourceID, ResourceType: grant.ResourceType, ResourceRef: agnetGrantResourceRef(grant), AllowedActions: append([]string{}, grant.PermissionScope...), Constraints: grant.Constraints, SecretRef: grant.SecretRef, Status: grant.Status, }) } } if manifest.ResourceGrants == nil { manifest.ResourceGrants = []agnetManifestGrant{} } return manifest } func buildAgnetAgentInstances(plan agnetOrchestrationPlan, phase string, runtimeState string) []agnetAgentInstance { instances := make([]agnetAgentInstance, 0) if len(plan.AgentRuntime.Agents) > 0 { for _, runtimeAgent := range plan.AgentRuntime.Agents { count := runtimeAgent.InstanceCount if count <= 0 { count = 1 } for i := 0; i < count; i++ { instances = append(instances, agnetAgentInstance{ InstanceID: "agi_" + common.GetUUID()[:12], Role: runtimeAgent.Role, Phase: phase, RuntimeState: runtimeState, }) } } } if len(instances) == 0 { for _, agent := range plan.Agents { instances = append(instances, agnetAgentInstance{ InstanceID: "agi_" + common.GetUUID()[:12], Role: agent.RoleTemplate, Phase: phase, RuntimeState: runtimeState, }) } } return instances } func planHasBindingScope(plan agnetOrchestrationPlan, bindingScope string) bool { bindingScope = strings.TrimSpace(bindingScope) if bindingScope == "" { return true } for _, agent := range plan.Agents { for _, grant := range agent.ResourceGrants { if strings.TrimSpace(grant.BindingScope) == bindingScope { return true } } } return false } func applyAuthenticatedManagerUserContext(c *gin.Context, plan *agnetOrchestrationPlan) { if plan == nil { return } if strings.TrimSpace(plan.UserContext.UserID) == "" { if userID := c.GetInt("id"); userID > 0 { plan.UserContext.UserID = fmt.Sprintf("%d", userID) } } if strings.TrimSpace(plan.UserContext.ChannelID) == "" { if group, ok := c.Get("group"); ok { if channelID, ok := group.(string); ok { plan.UserContext.ChannelID = strings.TrimSpace(channelID) } } } } 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 applyAuthenticatedManagerUserContext(c, &plan) if !validateOrchestrationPlan(c, plan) { return } now := agnetNow() deploymentID := "dep_" + common.GetUUID()[:12] record := agnetDeploymentRecord{ DeploymentID: deploymentID, Status: "accepted", Phase: "pending", RuntimeState: "queued", FailureReason: "", AgentInstances: buildAgnetAgentInstances(plan, "pending", "queued"), ResourceGrantManifest: buildAgnetPermissionManifest(plan), CreatedAt: now, UpdatedAt: now, Plan: plan, } event := agnetEvent{ EventID: "evt_" + common.GetUUID()[:12], Event: "deployment.accepted", SchemaVersion: 1, UserID: plan.UserContext.UserID, ChannelID: plan.UserContext.ChannelID, BindingScope: firstPlanBindingScope(plan), 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": record.Status, "phase": record.Phase, "runtime_state": record.RuntimeState, "failure_reason": record.FailureReason, "agent_instances": record.AgentInstances, "permission_manifest": record.ResourceGrantManifest, }) } 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) { userID := strings.TrimSpace(c.Query("user_id")) bindingScope := strings.TrimSpace(c.Query("binding_scope")) items := make([]agnetDeploymentRecord, 0) agnetMu.RLock() for _, record := range agnetDeployments { if userID != "" && record.Plan.UserContext.UserID != userID { continue } if bindingScope != "" && !planHasBindingScope(record.Plan, bindingScope) { 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.RuntimeState = "stopped" record.FailureReason = "" for i := range record.AgentInstances { record.AgentInstances[i].Phase = "stopped" record.AgentInstances[i].RuntimeState = "stopped" record.AgentInstances[i].FailureReason = "" } record.UpdatedAt = agnetNow() agnetDeployments[deploymentID] = record agnetEvents[deploymentID] = append(agnetEvents[deploymentID], agnetEvent{ EventID: "evt_" + common.GetUUID()[:12], Event: "instance.phase_changed", SchemaVersion: 1, UserID: record.Plan.UserContext.UserID, ChannelID: record.Plan.UserContext.ChannelID, BindingScope: firstPlanBindingScope(record.Plan), DeploymentID: deploymentID, CorrelationID: record.Plan.Metadata.CorrelationID, OccurredAt: agnetNow(), }) agnetMu.Unlock() common.ApiSuccess(c, gin.H{ "deployment_id": deploymentID, "status": "stopped", "phase": record.Phase, "runtime_state": record.RuntimeState, }) } 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 AgnetListDeploymentLogs(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] events := agnetEvents[deploymentID] agnetMu.RUnlock() if !ok { agnetError(c, "DEPLOYMENT_CONFLICT", "deployment not found") return } items := make([]gin.H, 0, len(events)+1) items = append(items, gin.H{ "timestamp": record.CreatedAt, "deployment_id": deploymentID, "stream": "control", "level": "info", "message": "deployment accepted by Manager control-plane placeholder", "phase": record.Phase, "runtime_state": record.RuntimeState, "failure_reason": record.FailureReason, "correlation_id": record.Plan.Metadata.CorrelationID, "redacted": true, }) for _, event := range events { items = append(items, gin.H{ "timestamp": event.OccurredAt, "deployment_id": event.DeploymentID, "stream": "event", "level": "info", "message": event.Event, "phase": record.Phase, "runtime_state": record.RuntimeState, "failure_reason": record.FailureReason, "correlation_id": event.CorrelationID, "redacted": true, }) } common.ApiSuccess(c, gin.H{ "deployment_id": deploymentID, "items": items, "next_cursor": "", "redacted": true, "total": len(items), }) } func AgnetGetDeploymentMetrics(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, gin.H{ "deployment_id": deploymentID, "window": strings.TrimSpace(c.DefaultQuery("window", "15m")), "step": strings.TrimSpace(c.DefaultQuery("step", "60s")), "phase": record.Phase, "status": record.Status, "runtime_state": record.RuntimeState, "failure_reason": record.FailureReason, "resource_usage": gin.H{ "cpu_percent": 0, "memory_bytes": 0, "network_rx_bytes": 0, "network_tx_bytes": 0, "task_duration_sec": 0, "platform_estimated": true, }, "series": []gin.H{}, }) } func AgnetProjectDashboardSnapshot(c *gin.Context) { bindingScope := strings.TrimSpace(c.Param("project_id")) if bindingScope == "" { agnetError(c, "POLICY_REJECTED", "binding_scope is required") return } active := 0 pending := 0 stopped := 0 agnetMu.RLock() for _, record := range agnetDeployments { if !planHasBindingScope(record.Plan, bindingScope) { continue } if record.Status == "accepted" { active++ } if record.Phase == "pending" { pending++ } if record.Phase == "stopped" { stopped++ } } agnetMu.RUnlock() common.ApiSuccess(c, gin.H{ "binding_scope": bindingScope, "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 := skSourceDisplayRef(source) if strings.TrimSpace(sourceRef) == "" { sourceRef = "ref_" + common.GetUUID()[:8] } if sourceType == "git" { sourceRef = sourceRef + "@sha_" + common.GetUUID()[:12] } snapshots = append(snapshots, agnetSKSnapshot{ SnapshotID: "sks_" + common.GetUUID()[:12], DeploymentID: deploymentID, UserID: record.Plan.UserContext.UserID, BindingScope: firstPlanBindingScope(record.Plan), 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, UserID: record.Plan.UserContext.UserID, ChannelID: record.Plan.UserContext.ChannelID, BindingScope: firstPlanBindingScope(record.Plan), 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) { userID := strings.TrimSpace(c.Query("user_id")) bindingScope := strings.TrimSpace(c.Query("binding_scope")) items := make([]gin.H, 0) agnetMu.RLock() for deploymentID, events := range agnetEvents { for _, event := range events { if userID != "" && event.UserID != userID { continue } if bindingScope != "" && event.BindingScope != bindingScope { continue } items = append(items, gin.H{ "actor": "agnet_control_plane", "action": event.Event, "resource": deploymentID, "user_id": event.UserID, "channel_id": event.ChannelID, "binding_scope": event.BindingScope, "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), }) }