Files
yuan-lab-llm--clawmanager/backend/internal/services/instance_runtime_status_service.go
T
heranran 71c7672545 feat: Skill Hub hardening with session usage tracking and egress governance (#163)
* feat(skill-hub): add Skill Hub catalog, publish flow, and lite materialize pipeline

Introduce Skill Hub for browsing, importing, publishing, and installing skills, with lite instance package materialization and runtime sync support.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): remove token-governance hooks from Skill Hub PR

Strip validateManagedRuntimeEnvironmentOverrides, network lock policy sync,
and egress proxy audit wiring that belong to the upcoming token-usage work,
so the Skill Hub branch compiles independently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): update migration number in materialize docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): make RuntimeAgentClient test stub and hub tests compile-safe

Add ResyncInstanceSkills to the runtime pool handler fake client, and harden
skill hub payload helpers/tests against nil storage/instance repos so go test passes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add Skill Hub hardening with session usage tracking and egress governance

Unify Skill Hub runtime sync improvements with session-token observability,
egress network policy, and admin/instance usage reporting for reopenable PR.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(skill-hub): repair CI tests and nested skill install

* fix(ci): restore release deployment configuration

---------

Co-authored-by: heshengran <heshengran@ieisystem.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 12:37:01 +08:00

214 lines
7.8 KiB
Go

package services
import (
"encoding/json"
"fmt"
"strings"
"time"
"clawreef/internal/models"
"clawreef/internal/repository"
)
type AgentStateReportRequest struct {
AgentID string `json:"agent_id" binding:"required"`
ReportedAt *time.Time `json:"reported_at,omitempty"`
Runtime AgentRuntimePayload `json:"runtime"`
SystemInfo map[string]interface{} `json:"system_info"`
Health map[string]interface{} `json:"health"`
}
type AgentRuntimePayload struct {
OpenClawStatus string `json:"openclaw_status"`
OpenClawPID *int `json:"openclaw_pid,omitempty"`
OpenClawVersion string `json:"openclaw_version"`
CurrentConfigRevisionID *int `json:"current_config_revision_id,omitempty"`
LLMConfigFingerprint *string `json:"llm_config_fingerprint,omitempty"`
LLMConfigStatus *string `json:"llm_config_status,omitempty"`
LLMProviderBaseURL *string `json:"llm_provider_base_url,omitempty"`
}
type InstanceRuntimeStatusPayload struct {
InstanceID int `json:"instance_id"`
InfraStatus string `json:"infra_status"`
AgentStatus string `json:"agent_status"`
OpenClawStatus string `json:"openclaw_status"`
OpenClawPID *int `json:"openclaw_pid,omitempty"`
OpenClawVersion *string `json:"openclaw_version,omitempty"`
CurrentConfigRevisionID *int `json:"current_config_revision_id,omitempty"`
DesiredConfigRevisionID *int `json:"desired_config_revision_id,omitempty"`
SystemInfo map[string]interface{} `json:"system_info,omitempty"`
Health map[string]interface{} `json:"health,omitempty"`
Summary map[string]interface{} `json:"summary,omitempty"`
LastReportedAt *time.Time `json:"last_reported_at,omitempty"`
}
type InstanceRuntimeStatusService interface {
Report(session *AgentSession, req AgentStateReportRequest, clientIP string) error
GetByInstanceID(instanceID int) (*InstanceRuntimeStatusPayload, error)
UpsertInfraStatus(instanceID int, infraStatus string) error
}
type instanceRuntimeStatusService struct {
runtimeRepo repository.InstanceRuntimeStatusRepository
agentRepo repository.InstanceAgentRepository
desiredStateRepo repository.InstanceDesiredStateRepository
}
func NewInstanceRuntimeStatusService(runtimeRepo repository.InstanceRuntimeStatusRepository, agentRepo repository.InstanceAgentRepository, desiredStateRepo repository.InstanceDesiredStateRepository) InstanceRuntimeStatusService {
return &instanceRuntimeStatusService{
runtimeRepo: runtimeRepo,
agentRepo: agentRepo,
desiredStateRepo: desiredStateRepo,
}
}
func (s *instanceRuntimeStatusService) Report(session *AgentSession, req AgentStateReportRequest, clientIP string) error {
if session == nil || session.Agent == nil || session.Instance == nil {
return fmt.Errorf("agent session is required")
}
if strings.TrimSpace(req.AgentID) != session.Agent.AgentID {
return fmt.Errorf("agent id does not match session")
}
status, err := s.getOrCreate(session.Instance.ID)
if err != nil {
return err
}
now := time.Now().UTC()
reportedAt := req.ReportedAt
if reportedAt == nil || reportedAt.IsZero() {
reportedAt = &now
}
status.AgentStatus = agentStatusOnline
if strings.TrimSpace(req.Runtime.OpenClawStatus) != "" {
status.OpenClawStatus = strings.TrimSpace(req.Runtime.OpenClawStatus)
}
status.OpenClawPID = req.Runtime.OpenClawPID
if strings.TrimSpace(req.Runtime.OpenClawVersion) != "" {
version := strings.TrimSpace(req.Runtime.OpenClawVersion)
status.OpenClawVersion = &version
}
status.CurrentConfigRevisionID = req.Runtime.CurrentConfigRevisionID
status.LastReportedAt = reportedAt
systemInfoJSON, err := marshalOptionalJSON(mergeLLMConfigIntoSystemInfo(req.SystemInfo, req.Runtime))
if err != nil {
return fmt.Errorf("failed to encode system info: %w", err)
}
healthJSON, err := marshalOptionalJSON(req.Health)
if err != nil {
return fmt.Errorf("failed to encode health info: %w", err)
}
status.SystemInfoJSON = systemInfoJSON
status.HealthJSON = healthJSON
if err := s.runtimeRepo.Update(status); err != nil {
return err
}
session.Agent.Status = agentStatusOnline
session.Agent.LastHeartbeatAt = reportedAt
session.Agent.LastReportedAt = reportedAt
session.Agent.LastSeenIP = optionalString(strings.TrimSpace(clientIP))
if err := s.agentRepo.Update(session.Agent); err != nil {
return err
}
return nil
}
func (s *instanceRuntimeStatusService) GetByInstanceID(instanceID int) (*InstanceRuntimeStatusPayload, error) {
status, err := s.runtimeRepo.GetByInstanceID(instanceID)
if err != nil {
return nil, err
}
if status == nil {
return nil, nil
}
payload := &InstanceRuntimeStatusPayload{
InstanceID: status.InstanceID,
InfraStatus: status.InfraStatus,
AgentStatus: status.AgentStatus,
OpenClawStatus: status.OpenClawStatus,
OpenClawPID: status.OpenClawPID,
OpenClawVersion: status.OpenClawVersion,
CurrentConfigRevisionID: status.CurrentConfigRevisionID,
DesiredConfigRevisionID: status.DesiredConfigRevisionID,
LastReportedAt: status.LastReportedAt,
}
if status.SystemInfoJSON != nil && strings.TrimSpace(*status.SystemInfoJSON) != "" {
if err := json.Unmarshal([]byte(*status.SystemInfoJSON), &payload.SystemInfo); err != nil {
return nil, fmt.Errorf("failed to decode system info: %w", err)
}
}
if status.HealthJSON != nil && strings.TrimSpace(*status.HealthJSON) != "" {
if err := json.Unmarshal([]byte(*status.HealthJSON), &payload.Health); err != nil {
return nil, fmt.Errorf("failed to decode health info: %w", err)
}
}
if status.SummaryJSON != nil && strings.TrimSpace(*status.SummaryJSON) != "" {
if err := json.Unmarshal([]byte(*status.SummaryJSON), &payload.Summary); err != nil {
return nil, fmt.Errorf("failed to decode runtime summary: %w", err)
}
}
return payload, nil
}
func mergeLLMConfigIntoSystemInfo(systemInfo map[string]interface{}, runtime AgentRuntimePayload) map[string]interface{} {
merged := map[string]interface{}{}
for key, value := range systemInfo {
merged[key] = value
}
if runtime.LLMConfigFingerprint != nil && strings.TrimSpace(*runtime.LLMConfigFingerprint) != "" {
merged["llm_config_fingerprint"] = strings.TrimSpace(*runtime.LLMConfigFingerprint)
}
if runtime.LLMConfigStatus != nil && strings.TrimSpace(*runtime.LLMConfigStatus) != "" {
merged["llm_config_status"] = strings.TrimSpace(*runtime.LLMConfigStatus)
}
if runtime.LLMProviderBaseURL != nil && strings.TrimSpace(*runtime.LLMProviderBaseURL) != "" {
merged["llm_provider_base_url"] = strings.TrimSpace(*runtime.LLMProviderBaseURL)
}
if len(merged) == 0 {
return nil
}
return merged
}
func (s *instanceRuntimeStatusService) UpsertInfraStatus(instanceID int, infraStatus string) error {
status, err := s.getOrCreate(instanceID)
if err != nil {
return err
}
status.InfraStatus = infraStatus
desiredState, err := s.desiredStateRepo.GetByInstanceID(instanceID)
if err == nil && desiredState != nil {
status.DesiredConfigRevisionID = desiredState.DesiredConfigRevisionID
}
return s.runtimeRepo.Update(status)
}
func (s *instanceRuntimeStatusService) getOrCreate(instanceID int) (*models.InstanceRuntimeStatus, error) {
status, err := s.runtimeRepo.GetByInstanceID(instanceID)
if err != nil {
return nil, err
}
if status != nil {
return status, nil
}
now := time.Now().UTC()
status = &models.InstanceRuntimeStatus{
InstanceID: instanceID,
InfraStatus: "creating",
AgentStatus: agentStatusOffline,
OpenClawStatus: openClawStatusUnknown,
CreatedAt: now,
UpdatedAt: now,
}
if err := s.runtimeRepo.Create(status); err != nil {
return nil, err
}
return status, nil
}