Skip to content

Commit 33caf49

Browse files
author
FTMahringer
committed
feat(plugins): add skills bundle plugin type
Add declarative skill and skill-bundle plugin support with local registry storage, manifest validation, and lifecycle handling.\n\nKeep skill plugins off the JVM loader path and advance core/dashboard defaults to 2.6.5-dev.
1 parent 48922a6 commit 33caf49

19 files changed

Lines changed: 463 additions & 18 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# v2.6.5-dev
2+
3+
Skills + Skill Bundle Plugin Type
4+
5+
## Added
6+
- Declarative `type: skill` and `type: skill-bundle` manifest support for skills.sh entries.
7+
- `SkillsBundleRegistry` for local skill bundle registration and inspection.
8+
- Validation for skills manifests and `skills.sh` source constraints.
9+
- Declarative install/uninstall handling without the JVM loader path.
10+
11+
## Changed
12+
- Core, dashboard, and plugin registry versions now advance to `2.6.5-dev`.
13+
- Skill plugins are treated as declarative external skill definitions.
14+
15+
## Fixed
16+
- JVM plugin loading now rejects `skill` and `skill-bundle` manifests with a clear error.

CHANGELOG.md

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

1010
---
1111

12+
## [v2.6.5-dev] - 2026-05-19
13+
14+
**Skills + Skill Bundle Plugin Type**
15+
16+
### Added
17+
- Declarative `type: skill` and `type: skill-bundle` manifest support with `skills.sh` entries.
18+
- `SkillsBundleRegistry` to register, inspect, and persist skill bundle metadata locally.
19+
- Manifest validation for required `skills` entries and `skills.sh` source constraints.
20+
- Install-time handling for declarative skill plugins without the Java classloader path.
21+
- Skill bundle lifecycle tests covering registration and sandbox bypass behavior.
22+
23+
### Changed
24+
- Core, dashboard, and plugin registry version defaults now advance to `2.6.5-dev`.
25+
- Declarative skill plugins are treated as external skill definitions instead of JVM-loaded plugins.
26+
27+
### Fixed
28+
- Skill bundle installs now fail with a clear message if a JVM load path is attempted.
29+
1230
## [v2.6.4-dev] - 2026-05-19
1331

1432
**MCP Server Plugin Type**

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> **Your AI. Your Rules. Your Stack.**
44
5-
**Current Release**: v2.6.4-dev (MCP Server Plugin Type)
5+
**Current Release**: v2.6.5-dev (Skills + Skill Bundle Plugin Type)
66

77
SYNAPSE is a self-hosted, provider-agnostic AI orchestration platform. Run it on your own infrastructure, connect local or remote model providers, manage workflows and runtime services, and extend everything through plugins, registries, and integrations — without depending on a third-party cloud or a built-in model.
88

docs/roadmaps/SYNAPSE_V3_IMPLEMENTATION_ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ security controls. Completes the plugin platform story.
471471
- No ClassLoader / no bytecode scan path
472472
- **Exit**: `filesystem-mcp` installs and connects via stdio transport
473473

474-
#### v2.6.5-dev: Skills + Skill Bundle Plugin Type
474+
#### v2.6.5-dev: Skills + Skill Bundle Plugin Type (completed)
475475
- skills.sh fetch + local skill storage
476476
- `SkillsBundleRegistry`
477477
- Atomic bundle install/uninstall (all-or-nothing)

packages/core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
<groupId>dev.synapse</groupId>
1717
<artifactId>synapse-core</artifactId>
18-
<version>2.6.4-dev</version>
18+
<version>2.6.5-dev</version>
1919
<name>synapse-core</name>
2020
<description>SYNAPSE Spring Boot backend</description>
2121

packages/core/src/main/java/dev/synapse/core/common/domain/Plugin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public enum PluginType {
8989
model,
9090
skill,
9191
mcp,
92+
skill_bundle,
9293
}
9394

9495
public enum PluginStatus {

packages/core/src/main/java/dev/synapse/plugins/ManifestValidator.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public ValidationResult validate(PluginManifest manifest) {
3737
}
3838

3939
if (manifest.type() == null) {
40-
errors.add("type is required and must be one of: channel, model, skill, mcp");
40+
errors.add("type is required and must be one of: channel, model, skill, skill-bundle, mcp");
4141
}
4242

4343
if (blank(manifest.version())) {
@@ -56,6 +56,11 @@ public ValidationResult validate(PluginManifest manifest) {
5656

5757
if (manifest.type() == dev.synapse.core.common.domain.Plugin.PluginType.mcp) {
5858
validateMcp(manifest, errors);
59+
} else if (
60+
manifest.type() == dev.synapse.core.common.domain.Plugin.PluginType.skill ||
61+
manifest.type() == dev.synapse.core.common.domain.Plugin.PluginType.skill_bundle
62+
) {
63+
validateSkills(manifest, errors);
5964
}
6065

6166
return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.fail(errors);
@@ -103,6 +108,44 @@ private void validateMcp(PluginManifest manifest, List<String> errors) {
103108
}
104109
}
105110

111+
private void validateSkills(PluginManifest manifest, List<String> errors) {
112+
if (manifest.skills() == null || manifest.skills().isEmpty()) {
113+
errors.add("skills section is required for type skill and skill-bundle");
114+
return;
115+
}
116+
117+
Object skillsObj = manifest.skills().get("skills");
118+
if (!(skillsObj instanceof List<?> skills) || skills.isEmpty()) {
119+
errors.add("skills.skills must be a non-empty list");
120+
return;
121+
}
122+
123+
if (manifest.type() == dev.synapse.core.common.domain.Plugin.PluginType.skill && skills.size() != 1) {
124+
errors.add("type skill requires exactly one skills entry");
125+
}
126+
127+
for (Object item : skills) {
128+
if (!(item instanceof Map<?, ?> skill)) {
129+
errors.add("each skills entry must be a map");
130+
continue;
131+
}
132+
String source = stringValue(skill.get("source"));
133+
String id = stringValue(skill.get("id"));
134+
String version = stringValue(skill.get("version"));
135+
if (blank(source)) {
136+
errors.add("skills source is required");
137+
} else if (!"skills.sh".equalsIgnoreCase(source)) {
138+
errors.add("skills source must be skills.sh");
139+
}
140+
if (blank(id)) {
141+
errors.add("skills id is required");
142+
}
143+
if (blank(version)) {
144+
errors.add("skills version is required");
145+
}
146+
}
147+
}
148+
106149
private String stringValue(Object value) {
107150
return value == null ? null : value.toString();
108151
}

packages/core/src/main/java/dev/synapse/plugins/PluginLifecycleService.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class PluginLifecycleService {
3939
private final PluginSandboxService sandboxService;
4040
private final PluginPerformanceMetrics performanceMetrics;
4141
private final McpServerRegistry mcpServerRegistry;
42+
private final SkillsBundleRegistry skillsBundleRegistry;
4243

4344
public PluginLifecycleService(
4445
PluginRepository pluginRepository,
@@ -50,7 +51,8 @@ public PluginLifecycleService(
5051
PluginDependencyResolver dependencyResolver,
5152
PluginSandboxService sandboxService,
5253
PluginPerformanceMetrics performanceMetrics,
53-
McpServerRegistry mcpServerRegistry
54+
McpServerRegistry mcpServerRegistry,
55+
SkillsBundleRegistry skillsBundleRegistry
5456
) {
5557
this.pluginRepository = pluginRepository;
5658
this.validator = validator;
@@ -62,6 +64,7 @@ public PluginLifecycleService(
6264
this.sandboxService = sandboxService;
6365
this.performanceMetrics = performanceMetrics;
6466
this.mcpServerRegistry = mcpServerRegistry;
67+
this.skillsBundleRegistry = skillsBundleRegistry;
6568
}
6669

6770
@Transactional
@@ -90,14 +93,14 @@ private Plugin doInstall(Map<String, Object> rawManifest) {
9093
plugin.setStatus(Plugin.PluginStatus.installed);
9194
plugin.setManifest(rawManifest);
9295
plugin.setStorageTier(
93-
manifest.type() == Plugin.PluginType.mcp
96+
isDeclarative(manifest.type())
9497
? Plugin.StorageTier.system
9598
: Plugin.StorageTier.staging
9699
);
97100
plugin.setLoaderState(Plugin.LoaderState.UNLOADED);
98101
plugin.setTrustTier(detectTrustTier(rawManifest));
99-
plugin.setSandboxEnabled(manifest.type() != Plugin.PluginType.mcp);
100-
if (manifest.type() == Plugin.PluginType.mcp) {
102+
plugin.setSandboxEnabled(!isDeclarative(manifest.type()));
103+
if (isDeclarative(manifest.type())) {
101104
plugin.setScanClean(true);
102105
}
103106

@@ -142,6 +145,11 @@ private Plugin doInstall(Map<String, Object> rawManifest) {
142145

143146
if (manifest.type() == Plugin.PluginType.mcp) {
144147
mcpServerRegistry.register(manifest);
148+
} else if (
149+
manifest.type() == Plugin.PluginType.skill ||
150+
manifest.type() == Plugin.PluginType.skill_bundle
151+
) {
152+
skillsBundleRegistry.register(manifest);
145153
}
146154

147155
logService.log(
@@ -233,6 +241,12 @@ public void uninstall(String id) {
233241
Plugin plugin = pluginRepository.findById(id).orElse(null);
234242
if (plugin != null && plugin.getType() == Plugin.PluginType.mcp) {
235243
mcpServerRegistry.unregister(id);
244+
} else if (
245+
plugin != null &&
246+
(plugin.getType() == Plugin.PluginType.skill ||
247+
plugin.getType() == Plugin.PluginType.skill_bundle)
248+
) {
249+
skillsBundleRegistry.unregister(id);
236250
}
237251

238252
storageService.deleteJar(id + ".jar");
@@ -264,6 +278,12 @@ private Plugin.TrustTier detectTrustTier(Map<String, Object> rawManifest) {
264278
return Plugin.TrustTier.COMMUNITY;
265279
}
266280

281+
private boolean isDeclarative(Plugin.PluginType type) {
282+
return type == Plugin.PluginType.mcp ||
283+
type == Plugin.PluginType.skill ||
284+
type == Plugin.PluginType.skill_bundle;
285+
}
286+
267287
@Transactional(readOnly = true)
268288
@Cacheable(value = "plugin-metadata", key = "'installed-plugins'")
269289
public List<Plugin> findAll() {

packages/core/src/main/java/dev/synapse/plugins/PluginManifest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public record PluginManifest(
2020
List<String> tags,
2121
List<PluginDependency> dependencies,
2222
List<PluginDependency> softDependencies,
23+
Map<String, Object> skills,
2324
Map<String, Object> mcp,
2425
Map<String, Object> configSchema,
2526
Map<String, Object> raw
@@ -29,7 +30,9 @@ public static PluginManifest fromMap(Map<String, Object> map) {
2930
Plugin.PluginType type = null;
3031
if (typeStr != null) {
3132
try {
32-
type = Plugin.PluginType.valueOf(typeStr.toLowerCase());
33+
type = Plugin.PluginType.valueOf(
34+
typeStr.toLowerCase().replace('-', '_')
35+
);
3336
} catch (IllegalArgumentException ignored) {}
3437
}
3538

@@ -61,6 +64,7 @@ public static PluginManifest fromMap(Map<String, Object> map) {
6164
tags,
6265
hardDeps,
6366
softDeps,
67+
mapOf(map.get("skills")),
6468
mapOf(map.get("mcp")),
6569
mapOf(map.get("config_schema")),
6670
map

packages/core/src/main/java/dev/synapse/plugins/PluginRegistryService.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public PluginRegistryService(
4848
RegistrySourceRepository registrySourceRepository,
4949
RegistryProperties registryProperties,
5050
RestClient restClient,
51-
@Value("${synapse.version:${spring.application.version:v2.6.4-dev}}") String synapseVersion
51+
@Value("${synapse.version:${spring.application.version:v2.6.5-dev}}") String synapseVersion
5252
) {
5353
this.storeRegistryService = storeRegistryService;
5454
this.registrySourceRepository = registrySourceRepository;
@@ -444,12 +444,15 @@ private boolean matchesType(StoreEntry entry, String type) {
444444
if (!StringUtils.hasText(type)) {
445445
return true;
446446
}
447-
String normalizedType = type.toLowerCase(Locale.ROOT);
447+
String normalizedType = type.toLowerCase(Locale.ROOT).replace('-', '_');
448448
if (entry.getType().name().equalsIgnoreCase(normalizedType)) {
449449
return true;
450450
}
451451
Object manifestType = entry.getMeta().get("type");
452-
return manifestType != null && normalizedType.equals(manifestType.toString().toLowerCase(Locale.ROOT));
452+
return manifestType != null &&
453+
normalizedType.equals(
454+
manifestType.toString().toLowerCase(Locale.ROOT).replace('-', '_')
455+
);
453456
}
454457

455458
boolean isCompatible(StoreEntry entry) {

0 commit comments

Comments
 (0)