Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
}
if status, ok := marketregistry.HTTPStatusCode(err); ok && status == http.StatusNotFound {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "artifact not found"})
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
}
if status, ok := marketregistry.HTTPStatusCode(err); ok && status == http.StatusNotFound {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "artifact not found"})
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.
applyMarketStatusFilter(&result, filter.Status)
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)
}

func applyMarketStatusFilter(result *marketplace.ListResult, status marketplace.ArtifactStatus) {
if result == nil || status == "" {
return
}
items := result.Items[:0]
for _, item := range result.Items {
if item.Status == status {
items = append(items, item)
}
}
result.Items = items
result.Total = len(items)
}

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