mirror of
https://github.com/hanzoai/agents.git
synced 2026-11-02 06:39:03 +00:00
926 lines
26 KiB
Go
926 lines
26 KiB
Go
package ui
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hanzoai/agents/control-plane/internal/services"
|
|
"github.com/hanzoai/agents/control-plane/internal/storage"
|
|
"github.com/hanzoai/agents/control-plane/pkg/types"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// DIDHandler provides handlers for UI-related DID operations.
|
|
type DIDHandler struct {
|
|
storage storage.StorageProvider
|
|
didService *services.DIDService
|
|
vcService *services.VCService
|
|
}
|
|
|
|
// NewDIDHandler creates a new DIDHandler.
|
|
func NewDIDHandler(storage storage.StorageProvider, didService *services.DIDService, vcService *services.VCService) *DIDHandler {
|
|
return &DIDHandler{
|
|
storage: storage,
|
|
didService: didService,
|
|
vcService: vcService,
|
|
}
|
|
}
|
|
|
|
// GetNodeDIDHandler handles requests for DID information about a specific node.
|
|
// GET /v1/ui/nodes/:nodeId/did
|
|
func (h *DIDHandler) GetNodeDIDHandler(c *gin.Context) {
|
|
nodeID := c.Param("nodeId")
|
|
if nodeID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "nodeId is required"})
|
|
return
|
|
}
|
|
|
|
// If DID service is not available, return empty response
|
|
if h.didService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_did": false,
|
|
"did_status": "inactive",
|
|
"reasoner_count": 0,
|
|
"skill_count": 0,
|
|
"last_updated": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get af server ID dynamically
|
|
hanzoAgentsServerID, err := h.didService.GetHanzoAgentsServerID()
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_did": false,
|
|
"did_status": "inactive",
|
|
"reasoner_count": 0,
|
|
"skill_count": 0,
|
|
"error": fmt.Sprintf("Failed to get af server ID: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get DID registry for the af server (not the node)
|
|
registry, err := h.didService.GetRegistry(hanzoAgentsServerID)
|
|
if err != nil || registry == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_did": false,
|
|
"did_status": "inactive",
|
|
"reasoner_count": 0,
|
|
"skill_count": 0,
|
|
"last_updated": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get agent info for this node
|
|
agentInfo, exists := registry.AgentNodes[nodeID]
|
|
if !exists {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_did": false,
|
|
"did_status": "inactive",
|
|
"reasoner_count": 0,
|
|
"skill_count": 0,
|
|
"last_updated": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Determine status
|
|
status := string(agentInfo.Status)
|
|
if agentInfo.DID == "" {
|
|
status = "inactive"
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"did": agentInfo.DID,
|
|
"agent_node_id": nodeID,
|
|
"hanzo_agents_server_id": registry.HanzoAgentsServerID,
|
|
"public_key_jwk": agentInfo.PublicKeyJWK,
|
|
"derivation_path": agentInfo.DerivationPath,
|
|
"reasoners": agentInfo.Reasoners,
|
|
"skills": agentInfo.Skills,
|
|
"status": status,
|
|
"registered_at": agentInfo.RegisteredAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// GetNodeVCStatusHandler handles requests for VC status information about a specific node.
|
|
// GET /v1/ui/nodes/:nodeId/vc-status
|
|
func (h *DIDHandler) GetNodeVCStatusHandler(c *gin.Context) {
|
|
nodeID := c.Param("nodeId")
|
|
if nodeID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "nodeId is required"})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return empty response
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vcs": false,
|
|
"vc_count": 0,
|
|
"verified_count": 0,
|
|
"last_vc_created": "",
|
|
"verification_status": "none",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get VCs for this node (filter by issuer DID or target DID)
|
|
filters := &types.VCFilters{
|
|
Limit: 1000, // Get all VCs for this node
|
|
}
|
|
|
|
executionVCs, err := h.vcService.QueryExecutionVCs(filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vcs": false,
|
|
"vc_count": 0,
|
|
"verified_count": 0,
|
|
"last_vc_created": "",
|
|
"verification_status": "none",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Filter VCs related to this node
|
|
var nodeVCs []types.ExecutionVC
|
|
for _, vc := range executionVCs {
|
|
// Check if this VC is related to the node (by issuer DID or target DID)
|
|
if vc.IssuerDID != "" && contains(vc.IssuerDID, nodeID) ||
|
|
vc.TargetDID != "" && contains(vc.TargetDID, nodeID) {
|
|
nodeVCs = append(nodeVCs, vc)
|
|
}
|
|
}
|
|
|
|
vcCount := len(nodeVCs)
|
|
verifiedCount := 0
|
|
var lastVCCreated string
|
|
|
|
for _, vc := range nodeVCs {
|
|
normalized := types.NormalizeExecutionStatus(vc.Status)
|
|
if normalized == string(types.ExecutionStatusSucceeded) || vc.Status == "verified" {
|
|
verifiedCount++
|
|
}
|
|
if lastVCCreated == "" || vc.CreatedAt.After(parseTime(lastVCCreated)) {
|
|
lastVCCreated = vc.CreatedAt.Format(time.RFC3339)
|
|
}
|
|
}
|
|
|
|
// Determine verification status
|
|
var verificationStatus string
|
|
if vcCount == 0 {
|
|
verificationStatus = "none"
|
|
} else if verifiedCount == vcCount {
|
|
verificationStatus = "verified"
|
|
} else if hasFailedVCs(nodeVCs) {
|
|
verificationStatus = "failed"
|
|
} else {
|
|
verificationStatus = "pending"
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vcs": vcCount > 0,
|
|
"vc_count": vcCount,
|
|
"verified_count": verifiedCount,
|
|
"last_vc_created": lastVCCreated,
|
|
"verification_status": verificationStatus,
|
|
})
|
|
}
|
|
|
|
// GetExecutionVCStatusHandler handles requests for VC status information about a specific execution.
|
|
// GET /v1/ui/executions/:executionId/vc-status
|
|
func (h *DIDHandler) GetExecutionVCStatusHandler(c *gin.Context) {
|
|
// Try both parameter names for compatibility with UI and Agent API routes
|
|
executionID := c.Param("executionId")
|
|
if executionID == "" {
|
|
executionID = c.Param("execution_id")
|
|
}
|
|
if executionID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "executionId or execution_id is required"})
|
|
return
|
|
}
|
|
|
|
// DEBUG: Log the execution ID being requested
|
|
fmt.Printf("DEBUG: GetExecutionVCStatusHandler called for execution_id: %s\n", executionID)
|
|
|
|
// If VC service is not available, return empty response
|
|
if h.vcService == nil {
|
|
fmt.Printf("DEBUG: VC service is nil for execution_id: %s\n", executionID)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vc": false,
|
|
"status": "none",
|
|
"created_at": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
executionVC, err := h.vcService.GetExecutionVCByExecutionID(executionID)
|
|
if err != nil {
|
|
fmt.Printf("DEBUG: Execution VC lookup failed for %s: %v\n", executionID, err)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vc": false,
|
|
"status": "none",
|
|
"created_at": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
fmt.Printf("DEBUG: Found VC for execution_id %s: vc_id=%s, status=%s, vc_document_type=%T\n",
|
|
executionID, executionVC.VCID, executionVC.Status, executionVC.VCDocument)
|
|
|
|
var vcDocumentForResponse interface{}
|
|
documentStatus := executionVC.Status
|
|
|
|
if len(executionVC.VCDocument) > 0 {
|
|
var parsed interface{}
|
|
if err := json.Unmarshal(executionVC.VCDocument, &parsed); err != nil {
|
|
fmt.Printf("DEBUG: VC document parsing failed for %s: %v\n", executionID, err)
|
|
vcDocumentForResponse = map[string]interface{}{
|
|
"parse_error": true,
|
|
"error": err.Error(),
|
|
"raw_length": len(executionVC.VCDocument),
|
|
"note": "VC document could not be parsed as valid JSON",
|
|
}
|
|
documentStatus = "malformed"
|
|
} else {
|
|
vcDocumentForResponse = parsed
|
|
fmt.Printf("DEBUG: VC document is valid JSON (%d bytes)\n", len(executionVC.VCDocument))
|
|
}
|
|
} else if executionVC.StorageURI != "" {
|
|
vcDocumentForResponse = map[string]interface{}{
|
|
"storage_uri": executionVC.StorageURI,
|
|
"document_size_bytes": executionVC.DocumentSize,
|
|
"note": "VC document stored via external URI",
|
|
}
|
|
documentStatus = "external"
|
|
} else {
|
|
documentStatus = "empty"
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"has_vc": true,
|
|
"vc_id": executionVC.VCID,
|
|
"status": documentStatus,
|
|
"original_status": executionVC.Status,
|
|
"created_at": executionVC.CreatedAt.Format(time.RFC3339),
|
|
"storage_uri": executionVC.StorageURI,
|
|
"document_size_bytes": executionVC.DocumentSize,
|
|
"vc_document": vcDocumentForResponse,
|
|
})
|
|
}
|
|
|
|
// GetExecutionVCHandler handles requests for VC information about a specific execution.
|
|
// GET /v1/ui/executions/:executionId/vc
|
|
func (h *DIDHandler) GetExecutionVCHandler(c *gin.Context) {
|
|
// Try both parameter names for compatibility with UI and Agent API routes
|
|
executionID := c.Param("executionId")
|
|
if executionID == "" {
|
|
executionID = c.Param("execution_id")
|
|
}
|
|
if executionID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "executionId or execution_id is required"})
|
|
return
|
|
}
|
|
|
|
// DEBUG: Log the execution ID being requested
|
|
fmt.Printf("DEBUG: GetExecutionVCHandler called for execution_id: %s\n", executionID)
|
|
|
|
// If VC service is not available, return error
|
|
if h.vcService == nil {
|
|
fmt.Printf("DEBUG: VC service is nil for execution_id: %s\n", executionID)
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "VC service not available"})
|
|
return
|
|
}
|
|
|
|
executionVC, err := h.vcService.GetExecutionVCByExecutionID(executionID)
|
|
if err != nil {
|
|
fmt.Printf("DEBUG: No VC found for execution_id: %s (err=%v)\n", executionID, err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "VC not found for this execution"})
|
|
return
|
|
}
|
|
|
|
if len(executionVC.VCDocument) == 0 {
|
|
if executionVC.StorageURI == "" {
|
|
fmt.Printf("DEBUG: VC document is empty for execution_id: %s\n", executionID)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "VC document not found or empty"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, executionVC)
|
|
}
|
|
|
|
type workflowVCStatusBatchRequest struct {
|
|
WorkflowIDs []string `json:"workflow_ids"`
|
|
}
|
|
|
|
// GetWorkflowVCStatusBatchHandler returns VC status summaries for multiple workflows.
|
|
// POST /v1/ui/workflows/vc-status
|
|
func (h *DIDHandler) GetWorkflowVCStatusBatchHandler(c *gin.Context) {
|
|
var req workflowVCStatusBatchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.WorkflowIDs) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflow_ids is required"})
|
|
return
|
|
}
|
|
|
|
result := make([]types.WorkflowVCStatusSummary, 0, len(req.WorkflowIDs))
|
|
|
|
if h.vcService == nil {
|
|
for _, id := range req.WorkflowIDs {
|
|
result = append(result, *types.DefaultWorkflowVCStatusSummary(id))
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"summaries": result})
|
|
return
|
|
}
|
|
|
|
summaryMap, err := h.vcService.GetWorkflowVCStatusSummaries(req.WorkflowIDs)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": fmt.Sprintf("failed to fetch workflow VC statuses: %v", err),
|
|
})
|
|
return
|
|
}
|
|
|
|
for _, id := range req.WorkflowIDs {
|
|
summary, ok := summaryMap[id]
|
|
if !ok || summary == nil {
|
|
summary = types.DefaultWorkflowVCStatusSummary(id)
|
|
}
|
|
result = append(result, *summary)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"summaries": result})
|
|
}
|
|
|
|
// GetWorkflowVCChainHandler handles requests for workflow VC chain information.
|
|
// GET /v1/ui/workflows/:workflowId/vc-chain
|
|
func (h *DIDHandler) GetWorkflowVCChainHandler(c *gin.Context) {
|
|
workflowID := c.Param("workflowId")
|
|
if workflowID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflowId is required"})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return empty response
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"workflow_id": workflowID,
|
|
"total_steps": 0,
|
|
"completed_steps": 0,
|
|
"status": "none",
|
|
"component_vcs": []interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get workflow VC chain
|
|
response, err := h.vcService.GetWorkflowVCChain(workflowID)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"workflow_id": workflowID,
|
|
"total_steps": 0,
|
|
"completed_steps": 0,
|
|
"status": "error",
|
|
"component_vcs": []interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// DownloadVCHandler handles requests to download a VC document.
|
|
// GET /v1/ui/vc/:vc_id/download
|
|
func (h *DIDHandler) DownloadVCHandler(c *gin.Context) {
|
|
vcID := c.Param("vc_id")
|
|
if vcID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "vc_id is required"})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return error
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "VC service not available"})
|
|
return
|
|
}
|
|
|
|
// Get VC document
|
|
filters := &types.VCFilters{
|
|
Limit: 1000,
|
|
}
|
|
|
|
executionVCs, err := h.vcService.QueryExecutionVCs(filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query VCs"})
|
|
return
|
|
}
|
|
|
|
// Find the VC
|
|
for _, vc := range executionVCs {
|
|
if vc.VCID == vcID {
|
|
// Set headers for file download
|
|
c.Header("Content-Type", "application/json")
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=vc-%s.json", vcID))
|
|
|
|
// Return the VC document
|
|
c.JSON(http.StatusOK, vc.VCDocument)
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "VC not found"})
|
|
}
|
|
|
|
// VerifyVCHandler handles requests to verify a VC.
|
|
// POST /v1/ui/vc/verify
|
|
func (h *DIDHandler) VerifyVCHandler(c *gin.Context) {
|
|
var req types.VCVerificationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Invalid request body",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return error
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "VC service not available"})
|
|
return
|
|
}
|
|
|
|
// Verify VC
|
|
response, err := h.vcService.VerifyVC(req.VCDocument)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to verify VC",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// VerifyExecutionVCComprehensiveHandler handles requests for comprehensive VC verification.
|
|
// POST /v1/ui/executions/:executionId/verify-vc
|
|
func (h *DIDHandler) VerifyExecutionVCComprehensiveHandler(c *gin.Context) {
|
|
// Try both parameter names for compatibility with UI and Agent API routes
|
|
executionID := c.Param("executionId")
|
|
if executionID == "" {
|
|
executionID = c.Param("execution_id")
|
|
}
|
|
if executionID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "executionId or execution_id is required"})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return error
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "VC service not available"})
|
|
return
|
|
}
|
|
|
|
// Perform comprehensive VC verification
|
|
result, err := h.vcService.VerifyExecutionVCComprehensive(executionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to verify VC",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// VerifyWorkflowVCComprehensiveHandler handles requests for comprehensive workflow VC verification.
|
|
// POST /v1/ui/workflows/:workflowId/verify-vc
|
|
func (h *DIDHandler) VerifyWorkflowVCComprehensiveHandler(c *gin.Context) {
|
|
workflowID := c.Param("workflowId")
|
|
if workflowID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "workflowId is required"})
|
|
return
|
|
}
|
|
|
|
// If VC service is not available, return error
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "VC service not available"})
|
|
return
|
|
}
|
|
|
|
// Perform comprehensive workflow VC verification
|
|
result, err := h.vcService.VerifyWorkflowVCComprehensive(workflowID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to verify workflow VC",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// ExportVCsHandler handles requests to export VCs with optional filtering.
|
|
// GET /v1/ui/did/export/vcs
|
|
func (h *DIDHandler) ExportVCsHandler(c *gin.Context) {
|
|
// If VC service is not available, return empty response
|
|
if h.vcService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"execution_vcs": []interface{}{},
|
|
"workflow_vcs": []interface{}{},
|
|
"total_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Parse query parameters for filtering
|
|
filters := &types.VCFilters{}
|
|
|
|
filters.Limit = 100 // default
|
|
if limit := c.Query("limit"); limit != "" {
|
|
if parsedLimit, err := strconv.Atoi(limit); err == nil {
|
|
filters.Limit = parsedLimit
|
|
}
|
|
}
|
|
|
|
if offset := c.Query("offset"); offset != "" {
|
|
if parsedOffset, err := strconv.Atoi(offset); err == nil {
|
|
filters.Offset = parsedOffset
|
|
}
|
|
}
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
filters.Status = &status
|
|
}
|
|
|
|
if workflowID := c.Query("workflow_id"); workflowID != "" {
|
|
filters.WorkflowID = &workflowID
|
|
}
|
|
if executionID := c.Query("execution_id"); executionID != "" {
|
|
filters.ExecutionID = &executionID
|
|
}
|
|
|
|
if sessionID := c.Query("session_id"); sessionID != "" {
|
|
filters.SessionID = &sessionID
|
|
}
|
|
|
|
// Get execution VCs
|
|
executionVCs, err := h.vcService.QueryExecutionVCs(filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"execution_vcs": []interface{}{},
|
|
"workflow_vcs": []interface{}{},
|
|
"total_count": 0,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get workflow VCs (if available)
|
|
workflowVCs := []interface{}{} // Placeholder - implement if needed
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"execution_vcs": executionVCs,
|
|
"workflow_vcs": workflowVCs,
|
|
"total_count": len(executionVCs),
|
|
})
|
|
}
|
|
|
|
// GetDIDSystemStatusHandler handles requests for DID system status.
|
|
// GET /v1/ui/did/status
|
|
func (h *DIDHandler) GetDIDSystemStatusHandler(c *gin.Context) {
|
|
if h.didService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "inactive",
|
|
"message": "DID system is not enabled",
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
})
|
|
return
|
|
}
|
|
|
|
// TODO: Implement actual DID system health check
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "active",
|
|
"message": "DID system is operational",
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// GetDIDResolutionBundleHandler handles requests for DID resolution bundle information.
|
|
// GET /v1/ui/did/:did/resolution-bundle
|
|
func (h *DIDHandler) GetDIDResolutionBundleHandler(c *gin.Context) {
|
|
did := c.Param("did")
|
|
if did == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "did is required"})
|
|
return
|
|
}
|
|
|
|
// If DID service is not available, return empty response
|
|
if h.didService == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"did": did,
|
|
"resolution_status": "inactive",
|
|
"did_document": nil,
|
|
"verification_keys": []interface{}{},
|
|
"service_endpoints": []interface{}{},
|
|
"related_vcs": []interface{}{},
|
|
"component_dids": []interface{}{},
|
|
"resolution_metadata": gin.H{
|
|
"resolved_at": time.Now().Format(time.RFC3339),
|
|
"resolver": "hanzo-agents-server",
|
|
"status": "inactive",
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get DID registry to find the DID
|
|
var didDocument interface{}
|
|
var verificationKeys []interface{}
|
|
var serviceEndpoints []interface{}
|
|
var componentDIDs []interface{}
|
|
var resolutionStatus string = "not_found"
|
|
|
|
// Try to find the DID in agent DIDs
|
|
agentDIDs, err := h.storage.ListAgentDIDs(c.Request.Context())
|
|
if err == nil {
|
|
for _, agentDID := range agentDIDs {
|
|
if agentDID.DID == did {
|
|
resolutionStatus = "resolved"
|
|
|
|
// Build DID document
|
|
didDocument = gin.H{
|
|
"@context": []string{
|
|
"https://www.w3.org/ns/did/v1",
|
|
"https://w3id.org/security/suites/jws-2020/v1",
|
|
},
|
|
"id": did,
|
|
"verificationMethod": []gin.H{
|
|
{
|
|
"id": did + "#key-1",
|
|
"type": "JsonWebKey2020",
|
|
"controller": did,
|
|
"publicKeyJwk": agentDID.PublicKeyJWK,
|
|
},
|
|
},
|
|
"authentication": []string{did + "#key-1"},
|
|
"assertionMethod": []string{did + "#key-1"},
|
|
"service": []gin.H{
|
|
{
|
|
"id": did + "#agent-service",
|
|
"type": "AgentService",
|
|
"serviceEndpoint": fmt.Sprintf("https://hanzo-agents-server/agents/%s", agentDID.AgentNodeID),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add verification keys
|
|
verificationKeys = append(verificationKeys, gin.H{
|
|
"id": did + "#key-1",
|
|
"type": "JsonWebKey2020",
|
|
"controller": did,
|
|
"publicKeyJwk": agentDID.PublicKeyJWK,
|
|
"purpose": []string{"authentication", "assertionMethod"},
|
|
})
|
|
|
|
// Add service endpoints
|
|
serviceEndpoints = append(serviceEndpoints, gin.H{
|
|
"id": did + "#agent-service",
|
|
"type": "AgentService",
|
|
"serviceEndpoint": fmt.Sprintf("https://hanzo-agents-server/agents/%s", agentDID.AgentNodeID),
|
|
})
|
|
|
|
// Add component DIDs (reasoners and skills)
|
|
for reasonerName, reasonerInfo := range agentDID.Reasoners {
|
|
componentDIDs = append(componentDIDs, gin.H{
|
|
"did": reasonerInfo.DID,
|
|
"type": "reasoner",
|
|
"name": reasonerName,
|
|
"capabilities": reasonerInfo.Capabilities,
|
|
"exposure_level": reasonerInfo.ExposureLevel,
|
|
})
|
|
}
|
|
|
|
for skillName, skillInfo := range agentDID.Skills {
|
|
componentDIDs = append(componentDIDs, gin.H{
|
|
"did": skillInfo.DID,
|
|
"type": "skill",
|
|
"name": skillName,
|
|
"tags": skillInfo.Tags,
|
|
"exposure_level": skillInfo.ExposureLevel,
|
|
})
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not found in agent DIDs, try component DIDs
|
|
if resolutionStatus == "not_found" {
|
|
componentDIDInfos, err := h.storage.ListComponentDIDs(c.Request.Context(), "")
|
|
if err == nil {
|
|
for _, componentDID := range componentDIDInfos {
|
|
if componentDID.ComponentDID == did {
|
|
resolutionStatus = "resolved"
|
|
|
|
// Build DID document for component
|
|
didDocument = gin.H{
|
|
"@context": []string{
|
|
"https://www.w3.org/ns/did/v1",
|
|
"https://w3id.org/security/suites/jws-2020/v1",
|
|
},
|
|
"id": did,
|
|
"verificationMethod": []gin.H{
|
|
{
|
|
"id": did + "#key-1",
|
|
"type": "JsonWebKey2020",
|
|
"controller": componentDID.AgentDID,
|
|
},
|
|
},
|
|
"authentication": []string{did + "#key-1"},
|
|
"assertionMethod": []string{did + "#key-1"},
|
|
"service": []gin.H{
|
|
{
|
|
"id": did + "#component-service",
|
|
"type": fmt.Sprintf("%sService", componentDID.ComponentType),
|
|
"serviceEndpoint": fmt.Sprintf("https://hanzo-agents-server/components/%s", componentDID.ComponentID),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add service endpoints
|
|
serviceEndpoints = append(serviceEndpoints, gin.H{
|
|
"id": did + "#component-service",
|
|
"type": fmt.Sprintf("%sService", componentDID.ComponentType),
|
|
"serviceEndpoint": fmt.Sprintf("https://hanzo-agents-server/components/%s", componentDID.ComponentID),
|
|
})
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get related VCs for this DID
|
|
var relatedVCs []interface{}
|
|
if h.vcService != nil {
|
|
filters := &types.VCFilters{
|
|
Limit: 100,
|
|
}
|
|
|
|
executionVCs, err := h.vcService.QueryExecutionVCs(filters)
|
|
if err == nil {
|
|
for _, vc := range executionVCs {
|
|
if vc.IssuerDID == did || vc.TargetDID == did || vc.CallerDID == did {
|
|
relatedVCs = append(relatedVCs, gin.H{
|
|
"vc_id": vc.VCID,
|
|
"execution_id": vc.ExecutionID,
|
|
"workflow_id": vc.WorkflowID,
|
|
"status": vc.Status,
|
|
"role": getDIDRole(did, vc),
|
|
"created_at": vc.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build resolution metadata
|
|
resolutionMetadata := gin.H{
|
|
"resolved_at": time.Now().Format(time.RFC3339),
|
|
"resolver": "hanzo-agents-server",
|
|
"status": resolutionStatus,
|
|
"method": "hanzo-agents",
|
|
}
|
|
|
|
if resolutionStatus == "resolved" {
|
|
resolutionMetadata["content_type"] = "application/did+ld+json"
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"did": did,
|
|
"resolution_status": resolutionStatus,
|
|
"did_document": didDocument,
|
|
"verification_keys": verificationKeys,
|
|
"service_endpoints": serviceEndpoints,
|
|
"related_vcs": relatedVCs,
|
|
"component_dids": componentDIDs,
|
|
"resolution_metadata": resolutionMetadata,
|
|
})
|
|
}
|
|
|
|
// DownloadDIDResolutionBundleHandler handles requests to download a DID resolution bundle.
|
|
// GET /v1/ui/did/:did/resolution-bundle/download
|
|
func (h *DIDHandler) DownloadDIDResolutionBundleHandler(c *gin.Context) {
|
|
did := c.Param("did")
|
|
if did == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "did is required"})
|
|
return
|
|
}
|
|
|
|
// Get the resolution bundle data (reuse the logic from GetDIDResolutionBundleHandler)
|
|
// This is a simplified version - in production, you might want to extract this to a shared function
|
|
|
|
// Set headers for file download
|
|
filename := fmt.Sprintf("did-resolution-bundle-%s.json", sanitizeDIDForFilename(did))
|
|
c.Header("Content-Type", "application/json")
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
|
|
|
// Get the bundle data by calling the resolution logic
|
|
// For now, we'll create a minimal bundle structure
|
|
bundle := gin.H{
|
|
"@context": []string{
|
|
"https://www.w3.org/ns/did/v1",
|
|
"https://w3id.org/security/suites/jws-2020/v1",
|
|
},
|
|
"did": did,
|
|
"resolution_metadata": gin.H{
|
|
"resolved_at": time.Now().Format(time.RFC3339),
|
|
"resolver": "hanzo-agents-server",
|
|
"method": "hanzo-agents",
|
|
},
|
|
"bundle_type": "did_resolution",
|
|
"generated_at": time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
// If DID service is available, get more detailed information
|
|
if h.didService != nil {
|
|
// Add more detailed resolution data here
|
|
bundle["status"] = "resolved"
|
|
} else {
|
|
bundle["status"] = "service_unavailable"
|
|
}
|
|
|
|
c.JSON(http.StatusOK, bundle)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func contains(s, substr string) bool {
|
|
return len(s) > 0 && len(substr) > 0 &&
|
|
(s == substr ||
|
|
(len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr)))
|
|
}
|
|
|
|
func parseTime(timeStr string) time.Time {
|
|
if timeStr == "" {
|
|
return time.Time{}
|
|
}
|
|
t, _ := time.Parse(time.RFC3339, timeStr)
|
|
return t
|
|
}
|
|
|
|
func hasFailedVCs(vcs []types.ExecutionVC) bool {
|
|
for _, vc := range vcs {
|
|
if types.NormalizeExecutionStatus(vc.Status) == string(types.ExecutionStatusFailed) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// getDIDRole determines the role of a DID in a VC (issuer, target, or caller)
|
|
func getDIDRole(did string, vc types.ExecutionVC) string {
|
|
if vc.IssuerDID == did {
|
|
return "issuer"
|
|
}
|
|
if vc.TargetDID == did {
|
|
return "target"
|
|
}
|
|
if vc.CallerDID == did {
|
|
return "caller"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// sanitizeDIDForFilename sanitizes a DID string to be safe for use in filenames
|
|
func sanitizeDIDForFilename(did string) string {
|
|
replacer := strings.NewReplacer(
|
|
":", "_",
|
|
"/", "_",
|
|
"\\", "_",
|
|
"?", "_",
|
|
"*", "_",
|
|
"<", "_",
|
|
">", "_",
|
|
"|", "_",
|
|
"\"", "_",
|
|
" ", "_",
|
|
)
|
|
|
|
sanitized := replacer.Replace(did)
|
|
if len(sanitized) > 100 {
|
|
sanitized = sanitized[:100]
|
|
}
|
|
|
|
return sanitized
|
|
}
|