Files
heicode-win/heicode/controller/agnet_control_plane.go
T
gongzhiyong ba02ae5be7 feat: align manager agnet boundaries
- add Manager user_context, NewAPI billing_context, and Agnet agent_runtime deployment fields

- move resource binding/grant scope toward user-owned binding_scope and secret_ref-only paths

- document OpenBao internal access and unified heicode.xinghanlab.com routing boundaries

- fix Manager session user id preservation after external auth login
2026-05-04 09:28:03 +08:00

932 lines
28 KiB
Go

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"`
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"`
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 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",
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": "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) {
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.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",
})
}
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,
"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,
"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,
"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),
})
}