package controller import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "os" "sort" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/heicode/manager/common" "github.com/heicode/manager/model" ) type agentCallbackEnvelope struct { EventID string `json:"event_id"` IdempotencyKey string `json:"idempotency_key"` EventType string `json:"event_type"` DeploymentID string `json:"deployment_id"` SwarmID string `json:"swarm_id"` AgentInstanceID string `json:"agent_instance_id"` TaskID string `json:"task_id"` OccurredAt string `json:"occurred_at"` CorrelationID string `json:"correlation_id"` Source string `json:"source"` Metadata map[string]any `json:"metadata"` Payload map[string]any `json:"payload"` Artifact agentArtifactPayload `json:"artifact"` } type agentArtifactPayload struct { ArtifactID string `json:"artifact_id"` ArtifactType string `json:"artifact_type"` Title string `json:"title"` Summary string `json:"summary"` URI string `json:"uri"` Checksum string `json:"checksum"` Metadata map[string]any `json:"metadata"` } func agentCallbackTokenFromRequest(c *gin.Context) string { if token := strings.TrimSpace(c.GetHeader("X-Agent-Service-Token")); token != "" { return token } auth := strings.TrimSpace(c.GetHeader("Authorization")) if strings.HasPrefix(strings.ToLower(auth), "bearer ") { return strings.TrimSpace(auth[len("Bearer "):]) } return "" } func agentCallbackSigningSecret() string { if secret := strings.TrimSpace(common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET", "")); secret != "" { return secret } secretRef := firstNonEmpty( common.GetEnvOrDefaultString("AGENT_CALLBACK_SIGNING_SECRET_REF", ""), common.GetEnvOrDefaultString("AGENT_RUNTIME_CALLBACK_SIGNING_SECRET_REF", ""), ) if secretRef == "" { return "" } client, err := newSecretStoreClientFromEnv() if err != nil { common.SysLog("agentCallbackSigningSecret: " + err.Error()) return "" } data, err := client.getJSONSecret(secretRef) if err != nil { common.SysLog("agentCallbackSigningSecret: " + err.Error()) return "" } for _, key := range []string{"callback_signing_secret", "signing_secret", "secret", "value"} { if value := callbackStringValue(data, key); value != "" { return value } } common.SysLog("agentCallbackSigningSecret: signing secret is missing from Azure Key Vault payload") return "" } func agentCallbackSignatureTolerance() time.Duration { seconds := common.GetEnvOrDefault("AGENT_CALLBACK_SIGNATURE_TOLERANCE_SECONDS", 300) if seconds <= 0 { seconds = 300 } return time.Duration(seconds) * time.Second } func validateAgentCallbackHMAC(c *gin.Context, rawBody []byte, eventID string) (bool, bool) { secret := agentCallbackSigningSecret() if secret == "" { return false, false } timestamp := strings.TrimSpace(c.GetHeader("X-Agent-Timestamp")) signature := strings.TrimSpace(c.GetHeader("X-Agent-Signature")) if timestamp == "" || signature == "" || eventID == "" { return false, false } tsMs, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback timestamp") return true, false } now := time.Now() eventTime := time.UnixMilli(tsMs) tolerance := agentCallbackSignatureTolerance() if eventTime.Before(now.Add(-tolerance)) || eventTime.After(now.Add(tolerance)) { agentError(c, "CALLBACK_UNAUTHORIZED", "callback timestamp outside allowed window") return true, false } payload := timestamp + "." + eventID + "." + string(rawBody) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(signature)) { agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback signature") return true, false } return true, true } func validateAgentCallbackAuth(c *gin.Context, rawBody []byte, eventID string) bool { if common.GetEnvOrDefaultBool("AGENT_CALLBACK_AUTH_DISABLED", false) { return true } if attempted, ok := validateAgentCallbackHMAC(c, rawBody, eventID); attempted { return ok } expected := strings.TrimSpace(os.Getenv("AGENT_CALLBACK_TOKEN")) if expected == "" { agentError(c, "CALLBACK_UNAUTHORIZED", "callback token is not configured") return false } if agentCallbackTokenFromRequest(c) != expected { agentError(c, "CALLBACK_UNAUTHORIZED", "invalid callback service token") return false } return true } func agentCallbackHasPlaintextSecret(payload agentCallbackEnvelope) bool { if containsPlaintextSecret(payload.Metadata) || containsPlaintextSecret(payload.Payload) || containsPlaintextSecret(payload.Artifact.Metadata) { return true } raw, _ := common.Marshal(payload) var asMap map[string]any if err := common.Unmarshal(raw, &asMap); err != nil { return false } return containsPlaintextSecret(asMap) } func agentCallbackDeploymentContext(deploymentID string, swarmID string) (agentDeploymentRecord, bool) { deploymentID = strings.TrimSpace(deploymentID) swarmID = strings.TrimSpace(swarmID) if deploymentID != "" { if record, ok := findAgentDeploymentRecord(deploymentID); ok { return record, true } } runtimeID := firstNonEmpty(swarmID, deploymentID) if runtimeID == "" { return agentDeploymentRecord{}, false } agentMu.RLock() for _, record := range agentDeployments { if strings.TrimSpace(record.RuntimeSwarmID) == runtimeID || strings.TrimSpace(record.RuntimeDeploymentID) == runtimeID { agentMu.RUnlock() return record, true } } agentMu.RUnlock() if model.DB == nil { return agentDeploymentRecord{}, false } var row model.AgentDeployment if err := model.DB.Where("runtime_swarm_id = ? OR runtime_deployment_id = ?", runtimeID, runtimeID).First(&row).Error; err != nil { return agentDeploymentRecord{}, false } record, err := agentDeploymentModelToRecord(row) if err != nil { common.SysLog("agentCallbackDeploymentContext: " + err.Error()) return agentDeploymentRecord{}, false } agentMu.Lock() agentDeployments[record.DeploymentID] = record agentMu.Unlock() return record, true } func callbackStringValue(values map[string]any, key string) string { if values == nil { return "" } value, ok := values[key] if !ok { return "" } switch typed := value.(type) { case string: return strings.TrimSpace(typed) case fmt.Stringer: return strings.TrimSpace(typed.String()) case float64: return strconv.FormatFloat(typed, 'f', -1, 64) case int: return strconv.Itoa(typed) case int64: return strconv.FormatInt(typed, 10) default: return "" } } func callbackMapValue(values map[string]any, key string) map[string]any { if values == nil { return nil } if nested, ok := values[key].(map[string]any); ok { return nested } return nil } func agentCallbackEventRuntimeState(eventType string, payload map[string]any) string { if state := callbackStringValue(payload, "status"); state != "" { return state } switch eventType { case "agent.started": return "running" case "agent.completed": return "completed" case "agent.crashed", "sk_tool.failed", "task.failed": return "failed" default: return "" } } func upsertAgentCallbackAgentInstance(record *agentDeploymentRecord, payload agentCallbackEnvelope, phase string, runtimeState string) bool { if record == nil { return false } instanceID := strings.TrimSpace(payload.AgentInstanceID) source := payload.Payload role := firstNonEmpty(callbackStringValue(source, "agent_role"), callbackStringValue(source, "role")) if role == "" && instanceID == "" { return false } failureReason := firstNonEmpty(callbackStringValue(source, "reason"), callbackStringValue(source, "error")) for idx := range record.AgentInstances { instance := &record.AgentInstances[idx] if instanceID != "" && strings.TrimSpace(instance.InstanceID) == instanceID { if role != "" { instance.Role = role } if phase != "" { instance.Phase = phase } if runtimeState != "" { instance.RuntimeState = runtimeState } if failureReason != "" { instance.FailureReason = failureReason } return true } if instanceID != "" && role != "" && strings.TrimSpace(instance.Role) == role && (strings.TrimSpace(instance.RuntimeState) == "queued" || strings.TrimSpace(instance.RuntimeState) == "pending") { instance.InstanceID = instanceID instance.Phase = firstNonEmpty(phase, instance.Phase) instance.RuntimeState = firstNonEmpty(runtimeState, instance.RuntimeState) instance.FailureReason = failureReason return true } } record.AgentInstances = append(record.AgentInstances, agentAgentInstance{ InstanceID: firstNonEmpty(instanceID, "agi_"+common.GetUUID()[:12]), Role: role, Phase: firstNonEmpty(phase, record.Phase), RuntimeState: firstNonEmpty(runtimeState, record.RuntimeState), FailureReason: failureReason, }) return true } func applyAgentCallbackDeploymentState(payload agentCallbackEnvelope, record agentDeploymentRecord) (agentDeploymentRecord, bool) { if strings.TrimSpace(record.DeploymentID) == "" { return record, false } source := payload.Payload changed := false switch payload.EventType { case "deployment.status_changed": status := callbackStringValue(source, "status") if status != "" { record.Status = status record.RuntimeState = status if containsString([]string{"completed", "failed", "stopped"}, status) { for idx := range record.AgentInstances { if record.AgentInstances[idx].RuntimeState == "" || !containsString([]string{"failed", "stopped"}, record.AgentInstances[idx].RuntimeState) { record.AgentInstances[idx].RuntimeState = status } if record.Phase != "" { record.AgentInstances[idx].Phase = record.Phase } } } changed = true } case "phase.changed": phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase")) checkpoint := callbackStringValue(source, "checkpoint") if phase != "" { record.Phase = phase changed = true } if checkpoint != "" { record.RuntimeState = checkpoint changed = true } case "timeline.updated": phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase")) checkpoint := callbackStringValue(source, "checkpoint") if phase != "" { record.Phase = phase changed = true } if checkpoint != "" && !containsString([]string{"completed", "failed", "stopped"}, record.RuntimeState) { record.RuntimeState = checkpoint changed = true } case "agent.started", "agent.completed", "agent.crashed": phase := firstNonEmpty(callbackStringValue(source, "stage"), callbackStringValue(source, "phase"), record.Phase) runtimeState := agentCallbackEventRuntimeState(payload.EventType, source) if upsertAgentCallbackAgentInstance(&record, payload, phase, runtimeState) { changed = true } } if !changed { return record, false } record.UpdatedAt = firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow()) agentMu.Lock() agentDeployments[record.DeploymentID] = record agentMu.Unlock() if err := updateAgentDeploymentRecord(record); err != nil { common.SysLog("applyAgentCallbackDeploymentState: " + err.Error()) } return record, true } func normalizeCallbackArtifact(payload *agentCallbackEnvelope) { if payload == nil || strings.TrimSpace(payload.Artifact.ArtifactID) != "" { return } source := payload.Payload if nested := callbackMapValue(source, "artifact"); nested != nil { source = nested } if len(source) == 0 { return } payload.Artifact = agentArtifactPayload{ ArtifactID: callbackStringValue(source, "artifact_id"), ArtifactType: callbackStringValue(source, "artifact_type"), Title: callbackStringValue(source, "title"), Summary: callbackStringValue(source, "summary"), URI: callbackStringValue(source, "uri"), Checksum: callbackStringValue(source, "checksum"), Metadata: map[string]any{}, } for key, value := range source { switch key { case "artifact_id", "artifact_type", "title", "summary", "uri", "checksum": continue case "metadata": if nested, ok := value.(map[string]any); ok { for nestedKey, nestedValue := range nested { payload.Artifact.Metadata[nestedKey] = nestedValue } } default: payload.Artifact.Metadata[key] = value } } if len(payload.Artifact.Metadata) == 0 { payload.Artifact.Metadata = nil } } func persistAgentArtifactFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error { artifact := payload.Artifact if strings.TrimSpace(artifact.ArtifactID) == "" { return nil } metadataJSON := "" if artifact.Metadata != nil { if data, err := common.Marshal(artifact.Metadata); err == nil { metadataJSON = string(data) } } return model.UpsertAgentArtifact(&model.AgentArtifact{ ArtifactID: strings.TrimSpace(artifact.ArtifactID), DeploymentID: strings.TrimSpace(payload.DeploymentID), TaskID: strings.TrimSpace(payload.TaskID), UserID: record.Plan.UserContext.UserID, BindingScope: firstPlanBindingScope(record.Plan), CorrelationID: strings.TrimSpace(payload.CorrelationID), ArtifactType: strings.TrimSpace(artifact.ArtifactType), Title: strings.TrimSpace(artifact.Title), Summary: strings.TrimSpace(artifact.Summary), URI: strings.TrimSpace(artifact.URI), Checksum: strings.TrimSpace(artifact.Checksum), MetadataJSON: metadataJSON, CreatedAtMs: time.Now().UnixMilli(), }) } func callbackBoolValue(values map[string]any, key string) bool { if values == nil { return false } switch typed := values[key].(type) { case bool: return typed case string: return strings.EqualFold(strings.TrimSpace(typed), "true") default: return false } } func callbackIntValue(values map[string]any, key string) int { if values == nil { return 0 } switch typed := values[key].(type) { case float64: return int(typed) case int: return typed case int64: return int(typed) case string: value, _ := strconv.Atoi(strings.TrimSpace(typed)) return value default: return 0 } } var agentCallbackEventRequiredFields = map[string][]string{ "deployment.status_changed": {"status"}, "phase.changed": {"stage", "checkpoint"}, "agent.started": {"agent_role"}, "agent.completed": {"agent_role"}, "agent.crashed": {"agent_role", "reason"}, "task.created": {"task_id", "title"}, "task.claimed": {"task_id", "agent_role"}, "task.running": {"task_id", "agent_role"}, "task.heartbeat": {"task_id", "agent_role"}, "task.blocked": {"task_id", "reason"}, "task.retried": {"task_id", "attempt"}, "task.released": {"task_id", "agent_role"}, "task.failed": {"task_id", "reason"}, "task.completed": {"task_id"}, "handoff.requested": {"task_id", "from_role", "to_role"}, "handoff.completed": {"task_id", "from_role", "to_role"}, "approval.requested": {"approval_id", "operation", "risk_level"}, "artifact.created": {"artifact_id"}, "timeline.updated": {"title"}, "sk_tool.called": {"tool_name", "tool_invocation_id"}, "sk_tool.completed": {"tool_name", "tool_invocation_id"}, "sk_tool.failed": {"tool_name", "tool_invocation_id", "reason"}, "budget.alert": {"threshold_pct"}, } var agentCallbackEventCategories = map[string]string{ "deployment.status_changed": "deployment", "phase.changed": "ordinary_sub", "agent.started": "ordinary_sub", "agent.completed": "ordinary_sub", "agent.crashed": "ordinary_sub", "task.created": "swarm_task_flow", "task.claimed": "swarm_task_flow", "task.running": "swarm_task_flow", "task.heartbeat": "swarm_task_flow", "task.blocked": "swarm_task_flow", "task.retried": "swarm_task_flow", "task.released": "swarm_task_flow", "task.failed": "swarm_task_flow", "task.completed": "swarm_task_flow", "handoff.requested": "swarm_task_flow", "handoff.completed": "swarm_task_flow", "approval.requested": "approval", "artifact.created": "artifact", "timeline.updated": "timeline", "sk_tool.called": "sk", "sk_tool.completed": "sk", "sk_tool.failed": "sk", "budget.alert": "budget", } func AgentGetRuntimeEventCallbackSchema(c *gin.Context) { events := make([]string, 0, len(agentCallbackEventRequiredFields)) for eventType := range agentCallbackEventRequiredFields { events = append(events, eventType) } sort.Strings(events) items := make([]gin.H, 0, len(events)) for _, eventType := range events { required := append([]string(nil), agentCallbackEventRequiredFields[eventType]...) sort.Strings(required) items = append(items, gin.H{ "event_type": eventType, "category": agentCallbackEventCategories[eventType], "required_fields": required, "payload_location": "top-level envelope or payload object; artifact_id may also be in artifact object", }) } common.ApiSuccess(c, gin.H{ "callback_path": "/api/agent/callbacks/runtime-events", "auth": gin.H{ "service_token_headers": []string{"X-Agent-Service-Token", "Authorization: Bearer "}, "hmac_headers": []string{"X-Agent-Event-Id", "X-Agent-Timestamp", "X-Agent-Signature"}, "hmac_payload": "timestamp + \".\" + event_id + \".\" + raw_body", }, "dedupe_keys": []string{"X-Agent-Event-Id", "event_id", "idempotency_key"}, "events": items, "security": gin.H{ "plaintext_secrets_allowed": false, "secret_ref_scheme": "azkv:///secrets/", }, }) } func callbackEnvelopeFieldValue(payload agentCallbackEnvelope, key string) string { switch key { case "task_id": return strings.TrimSpace(payload.TaskID) case "artifact_id": return strings.TrimSpace(payload.Artifact.ArtifactID) default: return "" } } func callbackHasFieldValue(payload agentCallbackEnvelope, key string) bool { if callbackEnvelopeFieldValue(payload, key) != "" { return true } if callbackStringValue(payload.Payload, key) != "" { return true } for _, nestedKey := range []string{"artifact", "approval"} { if nested := callbackMapValue(payload.Payload, nestedKey); nested != nil { if callbackStringValue(nested, key) != "" { return true } } } if key == "attempt" { return callbackIntValue(payload.Payload, key) > 0 } return false } func validateAgentCallbackEventSchema(payload agentCallbackEnvelope) error { required, ok := agentCallbackEventRequiredFields[payload.EventType] if !ok { return nil } for _, key := range required { if !callbackHasFieldValue(payload, key) { return fmt.Errorf("%s is required for %s callback", key, payload.EventType) } } return nil } func persistAgentApprovalFromCallback(payload agentCallbackEnvelope, record agentDeploymentRecord) error { if payload.EventType != "approval.requested" || model.DB == nil { return nil } source := payload.Payload if nested := callbackMapValue(source, "approval"); nested != nil { source = nested } approvalID := firstNonEmpty(callbackStringValue(source, "approval_id"), "appr_"+common.GetUUID()) userID, _ := strconv.Atoi(strings.TrimSpace(record.Plan.UserContext.UserID)) approval := model.AgentApprovalRequest{ ApprovalID: approvalID, UserId: userID, DeploymentID: strings.TrimSpace(payload.DeploymentID), BindingScope: firstNonEmpty(callbackStringValue(source, "binding_scope"), firstPlanBindingScope(record.Plan)), Operation: firstNonEmpty(callbackStringValue(source, "operation"), "runtime.approval"), ResourceID: firstNonEmpty(callbackStringValue(source, "resource_id"), strings.TrimSpace(payload.AgentInstanceID), strings.TrimSpace(payload.DeploymentID)), ResourceType: firstNonEmpty(callbackStringValue(source, "resource_type"), "custom"), ResourceScope: callbackStringValue(source, "resource_scope"), TargetRole: firstNonEmpty(callbackStringValue(source, "target_role"), callbackStringValue(source, "agent_role"), strings.TrimSpace(payload.AgentInstanceID), "runtime"), RiskLevel: firstNonEmpty(callbackStringValue(source, "risk_level"), "high"), RequiresCredential: callbackBoolValue(source, "requires_credential"), SecretRef: strings.TrimSpace(callbackStringValue(source, "secret_ref")), Status: agentApprovalStatusPending, RequestedBy: firstNonEmpty(callbackStringValue(source, "requested_by"), "agent-runtime"), RequestReason: firstNonEmpty(callbackStringValue(source, "reason"), callbackStringValue(source, "summary"), "Runtime requested approval"), TTLSeconds: callbackIntValue(source, "ttl_seconds"), } if approval.TTLSeconds <= 0 { approval.TTLSeconds = defaultAgentApprovalTTLSeconds } if approval.TTLSeconds > maxAgentApprovalTTLSeconds { approval.TTLSeconds = maxAgentApprovalTTLSeconds } if approval.SecretRef != "" && !strings.HasPrefix(approval.SecretRef, "azkv://") { return fmt.Errorf("approval secret_ref must use azkv:// Azure Key Vault reference") } if approval.RequiresCredential && approval.SecretRef == "" { return fmt.Errorf("approval secret_ref is required when requires_credential is true") } now := time.Now().UnixMilli() approval.ExpiresAt = now + int64(approval.TTLSeconds)*1000 var existing model.AgentApprovalRequest if err := model.DB.Where("approval_id = ?", approval.ApprovalID).First(&existing).Error; err == nil { return nil } if err := model.DB.Create(&approval).Error; err != nil { return err } recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "") return nil } func AgentReceiveRuntimeEventCallback(c *gin.Context) { rawBody, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { agentError(c, "CALLBACK_INVALID", "failed to read callback body") return } var payload agentCallbackEnvelope if err := common.Unmarshal(rawBody, &payload); err != nil { agentError(c, "CALLBACK_INVALID", err.Error()) return } if payload.EventID == "" { payload.EventID = strings.TrimSpace(c.GetHeader("X-Agent-Event-Id")) } if payload.CorrelationID == "" { payload.CorrelationID = strings.TrimSpace(c.GetHeader("X-Correlation-ID")) } payload.EventID = strings.TrimSpace(payload.EventID) payload.EventType = strings.TrimSpace(payload.EventType) if !validateAgentCallbackAuth(c, rawBody, payload.EventID) { return } if payload.EventID == "" || payload.EventType == "" { agentError(c, "CALLBACK_INVALID", "event_id and event_type are required") return } normalizeCallbackArtifact(&payload) if payload.IdempotencyKey == "" { payload.IdempotencyKey = payload.EventID } if err := validateAgentCallbackEventSchema(payload); err != nil { agentError(c, "CALLBACK_SCHEMA_INVALID", err.Error()) return } if agentCallbackHasPlaintextSecret(payload) { agentError(c, "CALLBACK_SECRET_REJECTED", "callbacks must not contain plaintext credential fields") return } incomingDeploymentID := strings.TrimSpace(payload.DeploymentID) incomingSwarmID := strings.TrimSpace(payload.SwarmID) record, _ := agentCallbackDeploymentContext(incomingDeploymentID, incomingSwarmID) if strings.TrimSpace(record.DeploymentID) != "" { payload.DeploymentID = record.DeploymentID if strings.TrimSpace(payload.SwarmID) == "" { payload.SwarmID = firstNonEmpty(record.RuntimeSwarmID, record.RuntimeDeploymentID, incomingDeploymentID) } } if payload.CorrelationID == "" { payload.CorrelationID = record.Plan.Metadata.CorrelationID } payloadJSON, _ := common.Marshal(payload) inserted, err := model.InsertAgentCallbackEvent(&model.AgentCallbackEvent{ EventID: payload.EventID, IdempotencyKey: strings.TrimSpace(payload.IdempotencyKey), CallbackType: "swarm-event", EventType: payload.EventType, DeploymentID: strings.TrimSpace(payload.DeploymentID), SwarmID: strings.TrimSpace(payload.SwarmID), AgentInstanceID: strings.TrimSpace(payload.AgentInstanceID), TaskID: strings.TrimSpace(payload.TaskID), UserID: record.Plan.UserContext.UserID, BindingScope: firstPlanBindingScope(record.Plan), CorrelationID: strings.TrimSpace(payload.CorrelationID), Source: strings.TrimSpace(payload.Source), Result: "ok", PayloadJSON: string(payloadJSON), OccurredAt: strings.TrimSpace(payload.OccurredAt), CreatedAtMs: time.Now().UnixMilli(), }) if err != nil { common.SysLog("AgentReceiveRuntimeEventCallback: " + err.Error()) agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist callback") return } if inserted { if err := persistAgentArtifactFromCallback(payload, record); err != nil { common.SysLog("persistAgentArtifactFromCallback: " + err.Error()) agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist artifact") return } if err := persistAgentApprovalFromCallback(payload, record); err != nil { common.SysLog("persistAgentApprovalFromCallback: " + err.Error()) agentError(c, "CALLBACK_PERSIST_FAILED", "failed to persist approval request") return } record, _ = applyAgentCallbackDeploymentState(payload, record) recordAgentAuditEvent(agentEvent{ EventID: "evt_" + common.GetUUID()[:12], Event: "callback." + payload.EventType, SchemaVersion: 1, UserID: record.Plan.UserContext.UserID, ChannelID: record.Plan.UserContext.ChannelID, BindingScope: firstPlanBindingScope(record.Plan), DeploymentID: strings.TrimSpace(payload.DeploymentID), CorrelationID: strings.TrimSpace(payload.CorrelationID), OccurredAt: firstNonEmpty(strings.TrimSpace(payload.OccurredAt), agentNow()), }, "agent_callback", strings.TrimSpace(payload.DeploymentID), agentRequestID(c), "ok") } common.ApiSuccess(c, gin.H{ "event_id": payload.EventID, "inserted": inserted, "idempotent": !inserted, "deduplicated": !inserted, "deployment_id": payload.DeploymentID, }) } func AgentListUserDeploymentArtifacts(c *gin.Context) { record, ok := requireAuthenticatedUserAgentDeployment(c) if !ok { return } items, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{ DeploymentID: record.DeploymentID, Limit: 500, }) if err != nil { common.SysLog("AgentListUserDeploymentArtifacts: " + err.Error()) agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts") return } common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "artifacts": items, "items": items, "total": len(items)}) } func AgentGetUserDeploymentArtifactContent(c *gin.Context) { record, ok := requireAuthenticatedUserAgentDeployment(c) if !ok { return } artifactID := strings.TrimSpace(c.Param("artifact_id")) if artifactID == "" { agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required") return } artifact, found, err := model.GetAgentArtifactByDeployment(record.DeploymentID, artifactID) if err != nil { common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error()) agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifact") return } if !found { agentError(c, "ARTIFACT_NOT_FOUND", "artifact not found") return } cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record)) if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" { agentError(c, "RUNTIME_NOT_CONFIGURED", "runtime is not configured") return } ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout) defer cancel() resp, err := callAgentRuntimeArtifactContent(ctx, cfg, record, artifact.ArtifactID) if err != nil { common.SysLog("AgentGetUserDeploymentArtifactContent: " + err.Error()) agentError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content") return } defer resp.Body.Close() headers := map[string]string{} for _, key := range []string{"Content-Disposition", "ETag", "Last-Modified", "Cache-Control"} { if value := strings.TrimSpace(resp.Header.Get(key)); value != "" { headers[key] = value } } contentType := strings.TrimSpace(resp.Header.Get("Content-Type")) if contentType == "" { contentType = "application/octet-stream" } c.DataFromReader(http.StatusOK, resp.ContentLength, contentType, resp.Body, headers) } func callbackPayloadMap(row model.AgentCallbackEvent) map[string]any { var payload agentCallbackEnvelope if err := common.UnmarshalJsonStr(row.PayloadJSON, &payload); err != nil { return nil } if payload.Payload != nil { return payload.Payload } return nil } func timelineEntryFromCallback(callback model.AgentCallbackEvent) gin.H { payload := callbackPayloadMap(callback) entry := gin.H{ "kind": "callback", "at": callback.CreatedAtMs, "event": callback.EventType, "event_id": callback.EventID, "event_type": callback.EventType, "deployment_id": callback.DeploymentID, "swarm_id": callback.SwarmID, "agent_instance_id": callback.AgentInstanceID, "occurred_at": callback.OccurredAt, "source": callback.Source, "payload": payload, } for _, key := range []string{"title", "summary", "stage", "checkpoint", "severity", "next_action", "agent_role", "task_id", "from_role", "to_role", "reason", "attempt"} { if value, ok := payload[key]; ok { entry[key] = value } } return entry } func AgentGetUserDeploymentTimeline(c *gin.Context) { record, ok := requireAuthenticatedUserAgentDeployment(c) if !ok { return } events, err := model.ListAgentAuditEventsByDeployment(record.DeploymentID) if err != nil { agentError(c, "TIMELINE_QUERY_FAILED", "failed to query audit events") return } callbacks, err := model.ListAgentCallbackEvents(model.ListAgentCallbackEventsFilter{DeploymentID: record.DeploymentID, Limit: 500}) if err != nil { agentError(c, "TIMELINE_QUERY_FAILED", "failed to query callbacks") return } artifacts, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{DeploymentID: record.DeploymentID, Limit: 500}) if err != nil { agentError(c, "TIMELINE_QUERY_FAILED", "failed to query artifacts") return } snapshots, err := model.ListAgentSKSnapshots(record.DeploymentID) if err != nil { agentError(c, "TIMELINE_QUERY_FAILED", "failed to query sk snapshots") return } timeline := make([]gin.H, 0, len(events)+len(callbacks)+len(artifacts)+len(snapshots)) for _, event := range events { timeline = append(timeline, gin.H{"kind": "audit", "at": event.OccurredAt, "event": event.Event}) } for _, callback := range callbacks { timeline = append(timeline, timelineEntryFromCallback(callback)) } for _, artifact := range artifacts { timeline = append(timeline, gin.H{"kind": "artifact", "at": artifact.CreatedAtMs, "event": artifact.ArtifactType, "artifact_id": artifact.ArtifactID}) } for _, snapshot := range snapshots { timeline = append(timeline, gin.H{"kind": "sk_snapshot", "at": snapshot.ResolvedAtMs, "event": snapshot.SourceType, "snapshot_id": snapshot.SnapshotID}) } common.ApiSuccess(c, gin.H{ "deployment_id": record.DeploymentID, "deployment": withDisplayStatus(record), "events": events, "callbacks": callbacks, "artifacts": artifacts, "sk_snapshots": snapshots, "timeline": timeline, }) }