Skip to content

Commit 9d1c9cb

Browse files
author
FTMahringer
committed
feat(registry): add plugin registry service core
1 parent fb2584d commit 9d1c9cb

26 files changed

Lines changed: 1124 additions & 18 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# v2.6.1-dev
2+
3+
Plugin Registry Service Core for the v2.7.0 milestone.
4+
5+
## Added
6+
7+
- `PluginRegistryService` with registry metadata search, version history lookup, compatibility checks, and artifact caching.
8+
- `/api/registry` endpoints for status, sync, versions, compatibility, and artifact serving.
9+
- `synapse registry` CLI commands:
10+
- `list`
11+
- `versions <pluginId>`
12+
- `status`
13+
- `sync`
14+
- Configurable registry mode and cache behavior through:
15+
- `SYNAPSE_REGISTRY_MODE`
16+
- `SYNAPSE_REGISTRY_PATH`
17+
- `SYNAPSE_REGISTRY_CACHE_DIR`
18+
- `SYNAPSE_REGISTRY_CACHE_TTL`
19+
- `SYNAPSE_REGISTRY_SYNC_INTERVAL`
20+
- Optional Compose `registry` profile that starts the registry service as a separate container.
21+
22+
## Validation
23+
24+
```bash
25+
cd packages/core
26+
mvn -q -Dtest=PluginRegistryServiceTest,PluginPerformanceBaselinesTest test
27+
28+
cd ../cli
29+
go test ./...
30+
```

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [v2.6.1-dev] - 2026-05-18
13+
14+
**Plugin Registry Service Core**
15+
16+
### Added
17+
- `PluginRegistryService` with metadata search, version listing, compatibility checks, and artifact proxy caching.
18+
- New registry API under `/api/registry` for status, manual sync, metadata queries, compatibility, versions, and artifact download/invalidation.
19+
- New `synapse registry` CLI command family with `list`, `versions`, `status`, and `sync`.
20+
- Configurable registry mode, cache directory, cache TTL, and background sync interval via `SYNAPSE_REGISTRY_*`.
21+
22+
### Changed
23+
- The `v2.6.1-dev` roadmap step is now complete.
24+
- Core and dashboard versions are now `2.6.1-dev`.
25+
26+
---
27+
1228
## [v2.6.0] - 2026-05-18
1329

1430
**Plugin Ecosystem Release**

docs/roadmaps/SYNAPSE_V3_IMPLEMENTATION_ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ security controls. Completes the plugin platform story.
442442

443443
### Implementation Steps
444444

445-
#### v2.6.1-dev: Plugin Registry Service Core
445+
#### v2.6.1-dev: Plugin Registry Service Core (completed)
446446
- Metadata API (search, versions, compatibility)
447447
- Artifact proxy + cache (JAR caching, cache TTL, manual invalidation)
448448
- Embedded mode (default) + standalone container mode via `SYNAPSE_REGISTRY_MODE`

installer/compose/docker-compose.prod.yml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ services:
4242

4343
backend:
4444
build:
45-
context: ../../packages/core
45+
context: ../..
46+
dockerfile: packages/core/Dockerfile
4647
restart: unless-stopped
4748
environment:
4849
SERVER_PORT: 8080
@@ -52,13 +53,50 @@ services:
5253
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-synapse}
5354
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
5455
ECHO_ENABLED: ${ECHO_ENABLED:-true}
56+
REDIS_HOST: redis
57+
REDIS_PORT: 6379
58+
SYNAPSE_STORE_REGISTRY_PATH: /app/store/registry.yml
59+
SYNAPSE_REGISTRY_MODE: ${SYNAPSE_REGISTRY_MODE:-embedded}
60+
SYNAPSE_REGISTRY_PATH: /app/store/registry.yml
61+
SYNAPSE_REGISTRY_CACHE_DIR: /app/registry-cache
5562
depends_on:
5663
postgres:
5764
condition: service_healthy
5865
redis:
5966
condition: service_healthy
6067
volumes:
6168
- ../../agents:/app/agents:ro
69+
- ../../store:/app/store:ro
70+
- registry-cache:/app/registry-cache
71+
networks:
72+
- synapse
73+
74+
registry:
75+
profiles:
76+
- registry
77+
build:
78+
context: ../..
79+
dockerfile: packages/core/Dockerfile
80+
restart: unless-stopped
81+
environment:
82+
SERVER_PORT: 8081
83+
SYSTEM_NAME: ${SYSTEM_NAME:-SYNAPSE}
84+
SYNAPSE_REGISTRY_MODE: standalone
85+
SYNAPSE_REGISTRY_PATH: /app/store/registry.yml
86+
SYNAPSE_REGISTRY_CACHE_DIR: /app/registry-cache
87+
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-synapse}
88+
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-synapse}
89+
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
90+
REDIS_HOST: redis
91+
REDIS_PORT: 6379
92+
depends_on:
93+
postgres:
94+
condition: service_healthy
95+
redis:
96+
condition: service_healthy
97+
volumes:
98+
- ../../store:/app/store:ro
99+
- registry-cache:/app/registry-cache
62100
networks:
63101
- synapse
64102

@@ -91,3 +129,4 @@ volumes:
91129
redis-data:
92130
qdrant-data:
93131
ollama-data:
132+
registry-cache:

installer/compose/docker-compose.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ services:
5757
REDIS_HOST: redis
5858
REDIS_PORT: 6379
5959
SYNAPSE_STORE_REGISTRY_PATH: /app/store/registry.yml
60+
SYNAPSE_REGISTRY_MODE: ${SYNAPSE_REGISTRY_MODE:-embedded}
61+
SYNAPSE_REGISTRY_PATH: /app/store/registry.yml
62+
SYNAPSE_REGISTRY_CACHE_DIR: /app/registry-cache
6063
ports:
6164
- "${BACKEND_PORT:-8080}:8080"
6265
depends_on:
@@ -67,6 +70,37 @@ services:
6770
volumes:
6871
- ../../agents:/app/agents:ro
6972
- ../../store:/app/store:ro
73+
- registry-cache:/app/registry-cache
74+
75+
registry:
76+
profiles:
77+
- registry
78+
build:
79+
context: ../..
80+
dockerfile: packages/core/Dockerfile
81+
environment:
82+
SERVER_PORT: 8081
83+
SYSTEM_NAME: ${SYSTEM_NAME:-SYNAPSE}
84+
SYNAPSE_REGISTRY_MODE: standalone
85+
SYNAPSE_REGISTRY_PATH: /app/store/registry.yml
86+
SYNAPSE_REGISTRY_CACHE_DIR: /app/registry-cache
87+
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-synapse}
88+
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-synapse}
89+
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-synapse_dev_password}
90+
REDIS_HOST: redis
91+
REDIS_PORT: 6379
92+
SECRETS_ENCRYPTION_KEY: ${SECRETS_ENCRYPTION_KEY:-dev_key_32_bytes_change_me_now!!}
93+
JWT_SECRET: ${JWT_SECRET:-CHANGE_ME_IN_PRODUCTION_THIS_MUST_BE_AT_LEAST_256_BITS_LONG_FOR_HS256}
94+
ports:
95+
- "${REGISTRY_PORT:-8081}:8081"
96+
depends_on:
97+
postgres:
98+
condition: service_healthy
99+
redis:
100+
condition: service_healthy
101+
volumes:
102+
- ../../store:/app/store:ro
103+
- registry-cache:/app/registry-cache
70104

71105
dashboard:
72106
build:
@@ -90,3 +124,4 @@ volumes:
90124
redis-data:
91125
qdrant-data:
92126
ollama-data:
127+
registry-cache:

packages/cli/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Commands:
2121
agents Manage agents (list, runtime, activate, pause)
2222
providers Manage model providers (list, test)
2323
plugins Manage plugins (list, enable, disable)
24-
store Browse store registry (list)
24+
registry Manage plugin registry metadata, sync, and status
2525
chat Conversations (list, new, send, interactive, messages)
2626
logs View system logs (list, stream)
2727
config Manage CLI profiles (list, set, show)
@@ -37,6 +37,8 @@ Examples:
3737
synapse auth login -u admin -w secret
3838
synapse health
3939
synapse agents list
40+
synapse registry sync
41+
synapse registry list --query ollama
4042
synapse chat new
4143
synapse chat send <convId> "Hello, agent"
4244
synapse chat interactive <convId>

packages/cli/cmd/list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestRootListShowsTopLevelCommandsOnly(t *testing.T) {
3535
}
3636

3737
out := buffers.stdout.String()
38-
for _, expected := range []string{"auth", "config", "plugins", "debug"} {
38+
for _, expected := range []string{"auth", "config", "plugins", "debug", "registry"} {
3939
if !strings.Contains(out, expected) {
4040
t.Fatalf("expected output to contain %q, got %q", expected, out)
4141
}

packages/cli/cmd/registry.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/synapse-dev/synapse-cli/internal/commandfeatures"
10+
tuioutput "github.com/synapse-dev/synapse-cli/internal/output"
11+
)
12+
13+
var registryCmd = &cobra.Command{
14+
Use: "registry",
15+
Short: "Manage the plugin registry service",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
handled, err := commandfeatures.HandleSubcommandListing(cmd, tuioutput.PrintSubcommands)
18+
if err != nil || handled {
19+
return err
20+
}
21+
22+
return cmd.Help()
23+
},
24+
}
25+
26+
var registryListCmd = &cobra.Command{
27+
Use: "list",
28+
Short: "List registry entries",
29+
RunE: func(cmd *cobra.Command, args []string) error {
30+
client := clientFromCmd(cmd)
31+
rawJSON, _ := cmd.Flags().GetBool("json")
32+
query, _ := cmd.Flags().GetString("query")
33+
source, _ := cmd.Flags().GetString("source")
34+
entryType, _ := cmd.Flags().GetString("type")
35+
compatibleOnly, _ := cmd.Flags().GetBool("compatible")
36+
37+
path := fmt.Sprintf(
38+
"/api/registry?q=%s&source=%s&type=%s&compatible=%t",
39+
url.QueryEscape(query),
40+
url.QueryEscape(source),
41+
url.QueryEscape(entryType),
42+
compatibleOnly,
43+
)
44+
45+
var resp struct {
46+
Entries []map[string]any `json:"entries"`
47+
Mode string `json:"mode"`
48+
}
49+
if err := client.Get(path, &resp); err != nil {
50+
return err
51+
}
52+
53+
if rawJSON {
54+
tuioutput.JSON(resp)
55+
return nil
56+
}
57+
58+
tuioutput.Header(fmt.Sprintf("Registry Entries (%d)", len(resp.Entries)))
59+
tuioutput.KV("Mode", resp.Mode)
60+
tuioutput.Separator()
61+
for _, entry := range resp.Entries {
62+
tuioutput.Row(
63+
fmt.Sprint(entry["id"]),
64+
fmt.Sprint(entry["name"]),
65+
fmt.Sprint(entry["type"]),
66+
fmt.Sprint(entry["version"]),
67+
fmt.Sprint(entry["source"]),
68+
)
69+
}
70+
return nil
71+
},
72+
}
73+
74+
var registryVersionsCmd = &cobra.Command{
75+
Use: "versions <pluginId>",
76+
Short: "Show known registry versions for an entry",
77+
Args: cobra.ExactArgs(1),
78+
RunE: func(cmd *cobra.Command, args []string) error {
79+
client := clientFromCmd(cmd)
80+
rawJSON, _ := cmd.Flags().GetBool("json")
81+
82+
var resp []map[string]any
83+
if err := client.Get("/api/registry/"+args[0]+"/versions", &resp); err != nil {
84+
return err
85+
}
86+
87+
if rawJSON {
88+
tuioutput.JSON(resp)
89+
return nil
90+
}
91+
92+
tuioutput.Header("Registry Versions: " + args[0])
93+
tuioutput.Separator()
94+
for _, version := range resp {
95+
tuioutput.Row(
96+
fmt.Sprint(version["version"]),
97+
fmt.Sprint(version["min_synapse"]),
98+
fmt.Sprint(version["artifact_url"]),
99+
)
100+
}
101+
return nil
102+
},
103+
}
104+
105+
var registrySyncCmd = &cobra.Command{
106+
Use: "sync",
107+
Short: "Trigger a registry sync now",
108+
RunE: func(cmd *cobra.Command, args []string) error {
109+
client := clientFromCmd(cmd)
110+
rawJSON, _ := cmd.Flags().GetBool("json")
111+
112+
var resp map[string]any
113+
if err := client.Post("/api/registry/sync", map[string]any{}, &resp); err != nil {
114+
return err
115+
}
116+
117+
if rawJSON {
118+
tuioutput.JSON(resp)
119+
return nil
120+
}
121+
122+
tuioutput.OK(
123+
fmt.Sprintf(
124+
"Registry synced: %v entries from %v (%v)",
125+
resp["entriesSynced"],
126+
resp["source"],
127+
resp["mode"],
128+
),
129+
)
130+
return nil
131+
},
132+
}
133+
134+
var registryStatusCmd = &cobra.Command{
135+
Use: "status",
136+
Short: "Show registry service status",
137+
RunE: func(cmd *cobra.Command, args []string) error {
138+
client := clientFromCmd(cmd)
139+
rawJSON, _ := cmd.Flags().GetBool("json")
140+
141+
var resp map[string]any
142+
if err := client.Get("/api/registry/status", &resp); err != nil {
143+
return err
144+
}
145+
146+
if rawJSON {
147+
tuioutput.JSON(resp)
148+
return nil
149+
}
150+
151+
tuioutput.Header("Registry Status")
152+
for _, key := range []string{"mode", "registryPath", "url", "artifactCacheDir", "artifactCacheTtl", "syncInterval"} {
153+
value := fmt.Sprint(resp[key])
154+
if strings.TrimSpace(value) == "" || value == "<nil>" {
155+
value = "-"
156+
}
157+
tuioutput.KV(key, value)
158+
}
159+
return nil
160+
},
161+
}
162+
163+
func init() {
164+
registryListCmd.Flags().StringP("query", "q", "", "Search query")
165+
registryListCmd.Flags().String("source", "", "Filter by source")
166+
registryListCmd.Flags().String("type", "", "Filter by type")
167+
registryListCmd.Flags().Bool("compatible", false, "Only show entries compatible with this SYNAPSE version")
168+
169+
registryCmd.AddCommand(registryListCmd, registryVersionsCmd, registrySyncCmd, registryStatusCmd)
170+
commandfeatures.BindSubcommandListing(registryCmd)
171+
rootCmd.AddCommand(registryCmd)
172+
}

0 commit comments

Comments
 (0)