mirror of
https://github.com/hanzoai/agents.git
synced 2026-11-02 06:39:03 +00:00
The server mounts /v1, /v1/ui and /v2/ui. The UI and its dev proxy, the helm and kubernetes health probes, functional tests, project templates, examples and scripts still called /api/v1, so the probes failed and the UI's requests missed. Co-authored-by: Hanzo Dev <[email protected]>
459 lines
14 KiB
Go
459 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/agents/control-plane/internal/logger"
|
|
"github.com/hanzoai/agents/control-plane/pkg/types"
|
|
)
|
|
|
|
// DIDService defines the DID operations required by handlers.
|
|
type DIDService interface {
|
|
RegisterAgent(req *types.DIDRegistrationRequest) (*types.DIDRegistrationResponse, error)
|
|
ResolveDID(did string) (*types.DIDIdentity, error)
|
|
ListAllAgentDIDs() ([]string, error)
|
|
}
|
|
|
|
// VCService defines the VC operations required by handlers.
|
|
type VCService interface {
|
|
VerifyVC(vcDocument json.RawMessage) (*types.VCVerificationResponse, error)
|
|
GetWorkflowVCChain(workflowID string) (*types.WorkflowVCChainResponse, error)
|
|
CreateWorkflowVC(workflowID, sessionID string, executionVCIDs []string) (*types.WorkflowVC, error)
|
|
GenerateExecutionVC(ctx *types.ExecutionContext, inputData, outputData []byte, status string, errorMessage *string, durationMS int) (*types.ExecutionVC, error)
|
|
QueryExecutionVCs(filters *types.VCFilters) ([]types.ExecutionVC, error)
|
|
ListWorkflowVCs() ([]*types.WorkflowVC, error)
|
|
GetExecutionVCByExecutionID(executionID string) (*types.ExecutionVC, error)
|
|
}
|
|
|
|
// DIDHandlers handles DID-related HTTP requests.
|
|
type DIDHandlers struct {
|
|
didService DIDService
|
|
vcService VCService
|
|
}
|
|
|
|
// NewDIDHandlers creates a new DID handlers instance.
|
|
func NewDIDHandlers(didService DIDService, vcService VCService) *DIDHandlers {
|
|
return &DIDHandlers{
|
|
didService: didService,
|
|
vcService: vcService,
|
|
}
|
|
}
|
|
|
|
// RegisterAgent handles agent DID registration requests.
|
|
// POST /v1/did/register
|
|
func (h *DIDHandlers) RegisterAgent(c *gin.Context) {
|
|
logger.Logger.Debug().Msg("🔍 DID registration endpoint called")
|
|
|
|
var req types.DIDRegistrationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
if req.AgentNodeID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "agent_node_id is required"})
|
|
return
|
|
}
|
|
|
|
response, err := h.didService.RegisterAgent(&req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register agent"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// ResolveDID handles DID resolution requests.
|
|
// GET /v1/did/resolve/:did
|
|
func (h *DIDHandlers) ResolveDID(c *gin.Context) {
|
|
did := c.Param("did")
|
|
if did == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "DID parameter is required"})
|
|
return
|
|
}
|
|
|
|
identity, err := h.didService.ResolveDID(did)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "DID not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"did": identity.DID,
|
|
"public_key_jwk": identity.PublicKeyJWK,
|
|
"component_type": identity.ComponentType,
|
|
"function_name": identity.FunctionName,
|
|
"derivation_path": identity.DerivationPath,
|
|
})
|
|
}
|
|
|
|
// VerifyVC handles VC verification requests.
|
|
// POST /v1/did/verify
|
|
func (h *DIDHandlers) VerifyVC(c *gin.Context) {
|
|
var req types.VCVerificationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
response, err := h.vcService.VerifyVC(req.VCDocument)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify VC"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// GetWorkflowVCChain handles workflow VC chain requests.
|
|
// GET /v1/did/workflow/:workflow_id/vc-chain
|
|
func (h *DIDHandlers) GetWorkflowVCChain(c *gin.Context) {
|
|
workflowID := c.Param("workflow_id")
|
|
logger.Logger.Debug().Msgf("🔍 GetWorkflowVCChain endpoint called for workflow: %s", workflowID)
|
|
|
|
if workflowID == "" {
|
|
logger.Logger.Debug().Msg("🔍 Missing workflow_id parameter")
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "workflow_id parameter is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
logger.Logger.Debug().Msgf("🔍 Calling VCService.GetWorkflowVCChain for workflow: %s", workflowID)
|
|
// Get workflow VC chain
|
|
response, err := h.vcService.GetWorkflowVCChain(workflowID)
|
|
if err != nil {
|
|
logger.Logger.Debug().Err(err).Msg("🔍 VCService.GetWorkflowVCChain failed")
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to get workflow VC chain",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
logger.Logger.Debug().Msg("🔍 Successfully retrieved workflow VC chain, returning response")
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// CreateWorkflowVC handles workflow VC creation requests.
|
|
// POST /v1/did/workflow/:workflow_id/vc
|
|
func (h *DIDHandlers) CreateWorkflowVC(c *gin.Context) {
|
|
workflowID := c.Param("workflow_id")
|
|
if workflowID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "workflow_id parameter is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
SessionID string `json:"session_id"`
|
|
ExecutionVCIDs []string `json:"execution_vc_ids"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Invalid request body",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Create workflow VC
|
|
workflowVC, err := h.vcService.CreateWorkflowVC(workflowID, req.SessionID, req.ExecutionVCIDs)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to create workflow VC",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, workflowVC)
|
|
}
|
|
|
|
// GetDIDStatus handles DID system status requests.
|
|
// GET /v1/did/status
|
|
func (h *DIDHandlers) GetDIDStatus(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "active",
|
|
"message": "DID system is operational",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// CreateExecutionVC handles execution VC creation requests from Python SDK.
|
|
// POST /v1/execution/vc
|
|
func (h *DIDHandlers) CreateExecutionVC(c *gin.Context) {
|
|
logger.Logger.Debug().Msg("🔍 DEBUG: CreateExecutionVC handler called")
|
|
|
|
var req struct {
|
|
ExecutionContext struct {
|
|
ExecutionID string `json:"execution_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
SessionID string `json:"session_id"`
|
|
CallerDID string `json:"caller_did"`
|
|
TargetDID string `json:"target_did"`
|
|
AgentNodeDID string `json:"agent_node_did"`
|
|
Timestamp string `json:"timestamp"`
|
|
} `json:"execution_context"`
|
|
InputData []byte `json:"input_data"`
|
|
OutputData []byte `json:"output_data"`
|
|
Status string `json:"status"`
|
|
ErrorMessage *string `json:"error_message"`
|
|
DurationMS int `json:"duration_ms"`
|
|
}
|
|
|
|
logger.Logger.Debug().Msg("🔍 DEBUG: About to parse JSON request body")
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
logger.Logger.Error().Err(err).Msg("🔍 DEBUG: JSON binding failed in CreateExecutionVC")
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Invalid request body",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
logger.Logger.Debug().
|
|
Str("execution_id", req.ExecutionContext.ExecutionID).
|
|
Str("workflow_id", req.ExecutionContext.WorkflowID).
|
|
Str("caller_did", req.ExecutionContext.CallerDID).
|
|
Str("target_did", req.ExecutionContext.TargetDID).
|
|
Str("status", req.Status).
|
|
Int("duration_ms", req.DurationMS).
|
|
Msg("🔍 DEBUG: Successfully parsed VC creation request")
|
|
|
|
// Parse timestamp
|
|
timestamp, err := time.Parse(time.RFC3339, req.ExecutionContext.Timestamp)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Invalid timestamp format",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Create execution context
|
|
execCtx := &types.ExecutionContext{
|
|
ExecutionID: req.ExecutionContext.ExecutionID,
|
|
WorkflowID: req.ExecutionContext.WorkflowID,
|
|
SessionID: req.ExecutionContext.SessionID,
|
|
CallerDID: req.ExecutionContext.CallerDID,
|
|
TargetDID: req.ExecutionContext.TargetDID,
|
|
AgentNodeDID: req.ExecutionContext.AgentNodeDID,
|
|
Timestamp: timestamp,
|
|
}
|
|
|
|
// Generate execution VC
|
|
executionVC, err := h.vcService.GenerateExecutionVC(
|
|
execCtx,
|
|
req.InputData,
|
|
req.OutputData,
|
|
req.Status,
|
|
req.ErrorMessage,
|
|
req.DurationMS,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to generate execution VC",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// If VC generation is disabled by config, return appropriate response
|
|
if executionVC == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "VC generation is disabled by configuration",
|
|
"execution_id": req.ExecutionContext.ExecutionID,
|
|
"workflow_id": req.ExecutionContext.WorkflowID,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return the VC data
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"vc_id": executionVC.VCID,
|
|
"execution_id": executionVC.ExecutionID,
|
|
"workflow_id": executionVC.WorkflowID,
|
|
"session_id": executionVC.SessionID,
|
|
"issuer_did": executionVC.IssuerDID,
|
|
"target_did": executionVC.TargetDID,
|
|
"caller_did": executionVC.CallerDID,
|
|
"vc_document": executionVC.VCDocument,
|
|
"signature": executionVC.Signature,
|
|
"input_hash": executionVC.InputHash,
|
|
"output_hash": executionVC.OutputHash,
|
|
"status": executionVC.Status,
|
|
"created_at": executionVC.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// ExportVCs handles VC export requests for external verification.
|
|
// GET /v1/did/export/vcs
|
|
func (h *DIDHandlers) ExportVCs(c *gin.Context) {
|
|
|
|
// Parse query parameters for filtering
|
|
var filters types.VCFilters
|
|
|
|
if workflowID := c.Query("workflow_id"); workflowID != "" {
|
|
filters.WorkflowID = &workflowID
|
|
}
|
|
if sessionID := c.Query("session_id"); sessionID != "" {
|
|
filters.SessionID = &sessionID
|
|
}
|
|
if issuerDID := c.Query("issuer_did"); issuerDID != "" {
|
|
filters.IssuerDID = &issuerDID
|
|
}
|
|
if status := c.Query("status"); status != "" {
|
|
filters.Status = &status
|
|
}
|
|
|
|
// Set default limit if not provided
|
|
if filters.Limit == 0 {
|
|
filters.Limit = 100
|
|
}
|
|
|
|
// Query execution VCs
|
|
executionVCs, err := h.vcService.QueryExecutionVCs(&filters)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to query execution VCs",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Query workflow VCs
|
|
workflowVCs, err := h.vcService.ListWorkflowVCs()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to query workflow VCs",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Get all registered agent DIDs from the DID service
|
|
agentDIDs, err := h.didService.ListAllAgentDIDs()
|
|
if err != nil {
|
|
logger.Logger.Debug().Err(err).Msg("🔍 Failed to list agent DIDs")
|
|
// Continue with empty list rather than failing the entire request
|
|
agentDIDs = []string{}
|
|
}
|
|
|
|
// Convert execution VCs to export format
|
|
var executionVCsExport []map[string]interface{}
|
|
for _, vc := range executionVCs {
|
|
// Convert execution VC to export format
|
|
executionVCsExport = append(executionVCsExport, map[string]interface{}{
|
|
"vc_id": vc.VCID,
|
|
"execution_id": vc.ExecutionID,
|
|
"workflow_id": vc.WorkflowID,
|
|
"session_id": vc.SessionID,
|
|
"issuer_did": vc.IssuerDID,
|
|
"target_did": vc.TargetDID,
|
|
"caller_did": vc.CallerDID,
|
|
"status": vc.Status,
|
|
"created_at": vc.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"agent_dids": agentDIDs,
|
|
"execution_vcs": executionVCsExport,
|
|
"workflow_vcs": workflowVCs,
|
|
"total_count": len(executionVCs) + len(workflowVCs),
|
|
"filters_applied": filters,
|
|
})
|
|
}
|
|
|
|
// GetDIDDocument handles DID document requests (W3C DID standard).
|
|
// GET /v1/did/document/:did
|
|
func (h *DIDHandlers) GetDIDDocument(c *gin.Context) {
|
|
did := c.Param("did")
|
|
if did == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "DID parameter is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Resolve DID to get identity information
|
|
identity, err := h.didService.ResolveDID(did)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "DID not found",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Parse public key JWK
|
|
var publicKeyJWK map[string]interface{}
|
|
if err := json.Unmarshal([]byte(identity.PublicKeyJWK), &publicKeyJWK); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Failed to parse public key",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Create W3C DID Document
|
|
didDocument := map[string]interface{}{
|
|
"@context": []string{
|
|
"https://www.w3.org/ns/did/v1",
|
|
"https://w3id.org/security/suites/ed25519-2020/v1",
|
|
},
|
|
"id": did,
|
|
"verificationMethod": []map[string]interface{}{
|
|
{
|
|
"id": did + "#key-1",
|
|
"type": "Ed25519VerificationKey2020",
|
|
"controller": did,
|
|
"publicKeyJwk": publicKeyJWK,
|
|
},
|
|
},
|
|
"authentication": []string{
|
|
did + "#key-1",
|
|
},
|
|
"assertionMethod": []string{
|
|
did + "#key-1",
|
|
},
|
|
"service": []map[string]interface{}{
|
|
{
|
|
"id": did + "#hanzo-agents-service",
|
|
"type": "HanzoAgentsAgentService",
|
|
"serviceEndpoint": "https://hanzo-agents.example.com/v1",
|
|
"description": "HanzoAgents Agent Platform Service",
|
|
},
|
|
},
|
|
}
|
|
|
|
c.JSON(http.StatusOK, didDocument)
|
|
}
|
|
|
|
// RegisterRoutes registers all DID-related routes.
|
|
func (h *DIDHandlers) RegisterRoutes(router zip.Router, bridge func(gin.HandlerFunc) zip.Handler) {
|
|
didGroup := router.Group("/did")
|
|
{
|
|
didGroup.Post("/register", bridge(h.RegisterAgent))
|
|
didGroup.Get("/resolve/:did", bridge(h.ResolveDID))
|
|
didGroup.Post("/verify", bridge(h.VerifyVC))
|
|
didGroup.Get("/workflow/:workflow_id/vc-chain", bridge(h.GetWorkflowVCChain))
|
|
didGroup.Post("/workflow/:workflow_id/vc", bridge(h.CreateWorkflowVC))
|
|
didGroup.Get("/status", bridge(h.GetDIDStatus))
|
|
didGroup.Get("/export/vcs", bridge(h.ExportVCs))
|
|
didGroup.Get("/document/:did", bridge(h.GetDIDDocument))
|
|
}
|
|
|
|
// Execution VC endpoint (separate from DID group to match Python SDK expectations)
|
|
router.Post("/execution/vc", bridge(h.CreateExecutionVC))
|
|
}
|