Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
nodepkg "github.com/1024XEngineer/anyclaw/pkg/gateway/resources/nodes"
inputlayer "github.com/1024XEngineer/anyclaw/pkg/input"
inputchannels "github.com/1024XEngineer/anyclaw/pkg/input/channels"
"github.com/1024XEngineer/anyclaw/pkg/marketplace"
marketregistry "github.com/1024XEngineer/anyclaw/pkg/marketplace/registry"
routeingress "github.com/1024XEngineer/anyclaw/pkg/route/ingress"
"github.com/1024XEngineer/anyclaw/pkg/runtime"
sessionrunner "github.com/1024XEngineer/anyclaw/pkg/runtime/sessionrunner"
Expand All @@ -45,6 +47,7 @@ type Server struct {
signal *inputchannels.SignalAdapter
ingress *routeingress.Service
runtimePool *runtime.RuntimePool
hotReload *runtime.HotReloadCoordinator
sessionRunner *sessionrunner.Manager
tasks *taskrunner.Manager
storeModule agentstore.StoreManager
Expand All @@ -69,6 +72,8 @@ type Server struct {
mcpRegistry *mcp.Registry
mcpServer *mcp.Server
marketStore *plugin.Store
marketJobs *marketplace.Store
marketRegistry *marketregistry.Client
discoverySvc *discovery.Service
mentionGate *inputlayer.MentionGate
groupSecurity *inputlayer.GroupSecurity
Expand Down
7 changes: 7 additions & 0 deletions pkg/gateway/gateway_constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
appsecurity "github.com/1024XEngineer/anyclaw/pkg/gateway/auth/security"
gatewaymiddleware "github.com/1024XEngineer/anyclaw/pkg/gateway/middleware"
nodepkg "github.com/1024XEngineer/anyclaw/pkg/gateway/resources/nodes"
"github.com/1024XEngineer/anyclaw/pkg/marketplace"
marketregistry "github.com/1024XEngineer/anyclaw/pkg/marketplace/registry"
"github.com/1024XEngineer/anyclaw/pkg/runtime"
sessionrunner "github.com/1024XEngineer/anyclaw/pkg/runtime/sessionrunner"
taskrunner "github.com/1024XEngineer/anyclaw/pkg/runtime/taskrunner"
Expand All @@ -36,8 +38,10 @@ func New(mainRuntime *runtime.MainRuntime) *Server {
jobMaxAttempts: mainRuntime.Config.Gateway.JobMaxAttempts,
webhooks: newWebhookHandler(),
nodes: newNodeManager(),
marketJobs: marketplace.NewStore(mainRuntime.WorkDir),
devicePairing: newDevicePairing(mainRuntime),
}
server.hotReload = runtime.NewHotReloadCoordinator(server.runtimePool, store)
server.approvals = state.NewApprovalManager(store)
server.sessionRunner = sessionrunner.NewManager(store, server.sessions, server.runtimePool, server.approvals, sessionEventRecorder{server: server})
server.tasks = taskrunner.NewManager(store, server.sessions, server.runtimePool, taskrunner.MainRuntimeInfo{
Expand All @@ -49,6 +53,9 @@ func New(mainRuntime *runtime.MainRuntime) *Server {
if sm, err := agentstore.NewStoreManager(mainRuntime.WorkDir, mainRuntime.ConfigPath); err == nil {
server.storeModule = sm
}
if mainRuntime.Config != nil && marketregistry.IsEnabled(mainRuntime.Config.Marketplace) {
server.marketRegistry = marketregistry.NewClientFromConfig(mainRuntime.Config.Marketplace)
}

server.openAICompat = newOpenAICompatHandler(server, mainRuntime)
return server
Expand Down
287 changes: 287 additions & 0 deletions pkg/gateway/gateway_market_artifacts_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package gateway

import (
"errors"
"net/http"
"strings"

"github.com/1024XEngineer/anyclaw/pkg/clihub"
"github.com/1024XEngineer/anyclaw/pkg/marketplace"
marketregistry "github.com/1024XEngineer/anyclaw/pkg/marketplace/registry"
)

func (s *Server) handleMarketArtifacts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if s == nil || s.mainRuntime == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "runtime not available"})
return
}

filter := marketplace.Filter{
Kind: marketplace.NormalizeKind(r.URL.Query().Get("kind")),
Source: marketplace.NormalizeSource(r.URL.Query().Get("source")),
Query: strings.TrimSpace(r.URL.Query().Get("q")),
Status: marketplace.NormalizeStatus(r.URL.Query().Get("status")),
Risk: strings.TrimSpace(r.URL.Query().Get("risk")),
Trust: strings.TrimSpace(r.URL.Query().Get("trust")),
Tag: strings.TrimSpace(r.URL.Query().Get("tag")),
Permission: strings.TrimSpace(r.URL.Query().Get("permission")),
Publisher: strings.TrimSpace(r.URL.Query().Get("publisher")),
OS: strings.TrimSpace(r.URL.Query().Get("os")),
Arch: strings.TrimSpace(r.URL.Query().Get("arch")),
Sort: strings.TrimSpace(r.URL.Query().Get("sort")),
Limit: parseIntParam(r.URL.Query().Get("limit"), 50),
Offset: parseIntParam(r.URL.Query().Get("offset"), 0),
}
if filter.Source == marketplace.SourceCloud {
result, cloudErr := s.listCloudMarketArtifacts(r, filter)
if cloudErr != "" {
writeJSON(w, http.StatusOK, map[string]any{
"data": result,
"meta": map[string]any{
"cloud_error": cloudErr,
},
})
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": result})
return
}
catalog := marketplace.NewLocalCatalog(marketplace.LocalCatalogDeps{
Config: s.mainRuntime.Config,
Skills: s.mainRuntime.Skills,
Plugins: s.plugins,
AgentStore: s.storeModule,
CLIHub: s.loadCLIHubCatalog(),
})
result, err := catalog.List(r.Context(), filter)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
result.Items = s.marketplaceStore().OverlayStatus(result.Items)

writeJSON(w, http.StatusOK, map[string]any{
"data": result,
})
}

func (s *Server) handleMarketArtifactDetail(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if s == nil || s.mainRuntime == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "runtime not available"})
return
}

id, versions := parseMarketArtifactPath(r.URL.Path)
if id == "" {
http.Error(w, "artifact id required", http.StatusBadRequest)
return
}

if s.shouldUseCloudMarketArtifact(r, id) {
if versions {
items, err := s.cloudMarketVersions(r, id)
if err != nil {
if errors.Is(err, marketregistry.ErrNotConfigured) || errors.Is(err, marketregistry.ErrRemoteDisabled) {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "cloud registry unavailable"})
return
}
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": items, "total": len(items)}})
return
}

artifact, err := s.cloudMarketArtifact(r, id)
if err != nil {
if errors.Is(err, marketregistry.ErrNotConfigured) || errors.Is(err, marketregistry.ErrRemoteDisabled) {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "cloud registry unavailable"})
return
}
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
Comment thread
h1177h marked this conversation as resolved.
return
}
overlaid := s.marketplaceStore().OverlayStatus([]marketplace.Artifact{*artifact})
if len(overlaid) > 0 {
artifact = &overlaid[0]
}
writeJSON(w, http.StatusOK, map[string]any{"data": artifact})
return
}

catalog := s.localMarketCatalog()

if versions {
items, err := catalog.Versions(r.Context(), id)
if err != nil {
if errors.Is(err, marketplace.ErrArtifactNotFound) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "artifact not found"})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": items}})
return
}

artifact, err := catalog.Get(r.Context(), id)
if err != nil {
if errors.Is(err, marketplace.ErrArtifactNotFound) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "artifact not found"})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}

overlaid := s.marketplaceStore().OverlayStatus([]marketplace.Artifact{*artifact})
if len(overlaid) > 0 {
artifact = &overlaid[0]
}
writeJSON(w, http.StatusOK, map[string]any{"data": artifact})
}

func (s *Server) localMarketCatalog() *marketplace.LocalCatalog {
return marketplace.NewLocalCatalog(marketplace.LocalCatalogDeps{
Config: s.mainRuntime.Config,
Skills: s.mainRuntime.Skills,
Plugins: s.plugins,
AgentStore: s.storeModule,
CLIHub: s.loadCLIHubCatalog(),
})
}

func (s *Server) cloudRegistryClient() *marketregistry.Client {
if s == nil || s.mainRuntime == nil || s.mainRuntime.Config == nil {
return nil
}
if !marketregistry.IsEnabled(s.mainRuntime.Config.Marketplace) {
return nil
}
if s.marketRegistry == nil {
s.marketRegistry = marketregistry.NewClientFromConfig(s.mainRuntime.Config.Marketplace)
}
return s.marketRegistry
}

func (s *Server) listCloudMarketArtifacts(r *http.Request, filter marketplace.Filter) (marketplace.ListResult, string) {
client := s.cloudRegistryClient()
if client == nil {
return emptyMarketList(filter), "cloud registry endpoint is not configured"
}
filter.Source = ""
result, err := client.List(r.Context(), filter)
if err != nil {
return emptyMarketList(filter), err.Error()
}
s.overlayCloudMarketStatus(r, &result, filter)
Comment thread
h1177h marked this conversation as resolved.
return result, ""
}

func (s *Server) overlayCloudMarketStatus(r *http.Request, result *marketplace.ListResult, filter marketplace.Filter) {
if s == nil || result == nil || len(result.Items) == 0 {
return
}
result.Items = s.marketplaceStore().OverlayStatus(result.Items)
localResult, err := s.localMarketCatalog().List(r.Context(), marketplace.Filter{
Kind: filter.Kind,
Source: marketplace.SourceLocal,
Limit: 1000,
})
Comment thread
h1177h marked this conversation as resolved.
Outdated
if err != nil || len(localResult.Items) == 0 {
return
}
byID := make(map[string]marketplace.Artifact, len(localResult.Items))
for _, item := range localResult.Items {
byID[strings.ToLower(strings.TrimSpace(item.ID))] = item
}
for i := range result.Items {
local, ok := byID[strings.ToLower(strings.TrimSpace(result.Items[i].ID))]
if !ok {
continue
}
result.Items[i].Status = local.Status
result.Items[i].Installed = local.Installed
result.Items[i].Bound = local.Bound
result.Items[i].Active = local.Active
result.Items[i].Enabled = local.Enabled
}
}

func (s *Server) cloudMarketArtifact(r *http.Request, id string) (*marketplace.Artifact, error) {
client := s.cloudRegistryClient()
if client == nil {
return nil, marketregistry.ErrNotConfigured
}
return client.Get(r.Context(), id)
}

func (s *Server) cloudMarketVersions(r *http.Request, id string) ([]marketplace.ArtifactVersion, error) {
client := s.cloudRegistryClient()
if client == nil {
return nil, marketregistry.ErrNotConfigured
}
return client.Versions(r.Context(), id)
}

func (s *Server) shouldUseCloudMarketArtifact(r *http.Request, id string) bool {
trimmed := strings.TrimSpace(id)
if trimmed == "" {
return false
}
if strings.EqualFold(r.URL.Query().Get("source"), string(marketplace.SourceCloud)) {
return true
}
if strings.HasPrefix(strings.ToLower(trimmed), "cloud.") {
return true
}
if s.cloudRegistryClient() == nil {
return false
}
_, err := s.localMarketCatalog().Get(r.Context(), trimmed)
return errors.Is(err, marketplace.ErrArtifactNotFound)
}

func emptyMarketList(filter marketplace.Filter) marketplace.ListResult {
limit := filter.Limit
if limit <= 0 {
limit = 50
}
offset := filter.Offset
if offset < 0 {
offset = 0
}
return marketplace.ListResult{Items: []marketplace.Artifact{}, Total: 0, Limit: limit, Offset: offset}
}

func parseMarketArtifactPath(path string) (string, bool) {
trimmed := strings.Trim(strings.TrimPrefix(path, "/market/artifacts/"), "/")
if trimmed == "" || trimmed == path {
return "", false
}
parts := strings.Split(trimmed, "/")
if len(parts) == 2 && parts[1] == "versions" {
return parts[0], true
}
return parts[0], false
}

func (s *Server) loadCLIHubCatalog() *clihub.Catalog {
if s == nil || s.mainRuntime == nil {
return nil
}
catalog, err := clihub.LoadAuto(s.mainRuntime.WorkingDir)
if err != nil {
return nil
}
return catalog
}
Loading
Loading