diff --git a/src/main/java/io/naftiko/Capability.java b/src/main/java/io/naftiko/Capability.java
index 2d8f2a8..dfc54a4 100644
--- a/src/main/java/io/naftiko/Capability.java
+++ b/src/main/java/io/naftiko/Capability.java
@@ -28,12 +28,14 @@
import io.naftiko.engine.exposes.ApiServerAdapter;
import io.naftiko.engine.exposes.McpServerAdapter;
import io.naftiko.engine.exposes.ServerAdapter;
+import io.naftiko.engine.exposes.SkillServerAdapter;
import io.naftiko.spec.NaftikoSpec;
import io.naftiko.spec.consumes.ClientSpec;
import io.naftiko.spec.consumes.HttpClientSpec;
import io.naftiko.spec.exposes.ApiServerSpec;
import io.naftiko.spec.exposes.McpServerSpec;
import io.naftiko.spec.exposes.ServerSpec;
+import io.naftiko.spec.exposes.SkillServerSpec;
/**
* Main Capability class that initializes and manages adapters based on configuration
@@ -88,6 +90,8 @@ public String getVariable(String key) {
this.serverAdapters.add(new ApiServerAdapter(this, (ApiServerSpec) serverSpec));
} else if ("mcp".equals(serverSpec.getType())) {
this.serverAdapters.add(new McpServerAdapter(this, (McpServerSpec) serverSpec));
+ } else if ("skill".equals(serverSpec.getType())) {
+ this.serverAdapters.add(new SkillServerAdapter(this, (SkillServerSpec) serverSpec));
}
}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java b/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java
new file mode 100644
index 0000000..f0133b1
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillCatalogResource.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+import io.naftiko.spec.exposes.SkillToolSpec;
+
+/**
+ * Handles {@code GET /skills} — lists all skills with their tool name summaries.
+ *
+ *
+ */
+public class SkillCatalogResource extends SkillServerResource {
+
+ @Get("json")
+ public Representation getCatalog() {
+ ArrayNode skillList = getMapper().createArrayNode();
+
+ for (ExposedSkillSpec skill : getSkillServerSpec().getSkills()) {
+ ObjectNode entry = getMapper().createObjectNode();
+ entry.put("name", skill.getName());
+ entry.put("description", skill.getDescription());
+ if (skill.getLicense() != null) {
+ entry.put("license", skill.getLicense());
+ }
+ ArrayNode toolNames = getMapper().createArrayNode();
+ for (SkillToolSpec tool : skill.getTools()) {
+ toolNames.add(tool.getName());
+ }
+ entry.set("tools", toolNames);
+ skillList.add(entry);
+ }
+
+ ObjectNode response = getMapper().createObjectNode();
+ response.put("count", skillList.size());
+ response.set("skills", skillList);
+
+ return new JacksonRepresentation<>(response);
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java b/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java
new file mode 100644
index 0000000..4fd4d18
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillContentsResource.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import org.restlet.resource.ResourceException;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+
+/**
+ * Handles {@code GET /skills/{name}/contents} — lists all files in the skill's {@code location}
+ * directory.
+ *
+ *
Returns 404 if the skill is not found or has no {@code location} configured.
+ */
+public class SkillContentsResource extends SkillServerResource {
+
+ @Get("json")
+ public Representation listContents() throws Exception {
+ String name = getAttribute("name");
+ ExposedSkillSpec skill = findSkill(name);
+ if (skill == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name);
+ }
+ if (skill.getLocation() == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "No location configured for skill: " + name);
+ }
+
+ Path root = Paths.get(URI.create(skill.getLocation())).normalize().toAbsolutePath();
+ if (!Files.exists(root) || !Files.isDirectory(root)) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "Location directory not found for skill: " + name);
+ }
+
+ ArrayNode fileList = getMapper().createArrayNode();
+ try (Stream stream = Files.walk(root)) {
+ stream.filter(Files::isRegularFile).sorted().forEach(file -> {
+ ObjectNode entry = getMapper().createObjectNode();
+ entry.put("path", root.relativize(file).toString().replace('\\', '/'));
+ try {
+ entry.put("size", Files.size(file));
+ } catch (Exception e) {
+ entry.put("size", 0);
+ }
+ entry.put("type", detectMediaType(file.getFileName().toString()).getName());
+ fileList.add(entry);
+ });
+ }
+
+ ObjectNode response = getMapper().createObjectNode();
+ response.put("name", name);
+ response.set("files", fileList);
+
+ return new JacksonRepresentation<>(response);
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java
new file mode 100644
index 0000000..af7724b
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillDetailResource.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.util.Map;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import org.restlet.resource.ResourceException;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+import io.naftiko.spec.exposes.SkillToolSpec;
+
+/**
+ * Handles {@code GET /skills/{name}} — returns full skill metadata and tool catalog.
+ *
+ *
For derived tools, the response includes an {@code invocationRef} so agents
+ * know which sibling adapter to call and which operation/tool to invoke. For
+ * instruction tools, the response includes the {@code instruction} file path that
+ * agents can download via {@code GET /skills/{name}/contents/{file}}.
+ *
+ *
Returns 404 if the skill name is not found.
+ */
+public class SkillDetailResource extends SkillServerResource {
+
+ @Get("json")
+ public Representation getSkill() {
+ String name = getAttribute("name");
+ ExposedSkillSpec skill = findSkill(name);
+ if (skill == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name);
+ }
+
+ ObjectNode response = getMapper().createObjectNode();
+ response.put("name", skill.getName());
+ response.put("description", skill.getDescription());
+ if (skill.getLicense() != null) {
+ response.put("license", skill.getLicense());
+ }
+ if (skill.getCompatibility() != null) {
+ response.put("compatibility", skill.getCompatibility());
+ }
+ if (skill.getArgumentHint() != null) {
+ response.put("argument-hint", skill.getArgumentHint());
+ }
+ if (skill.getAllowedTools() != null) {
+ response.put("allowed-tools", skill.getAllowedTools());
+ }
+ if (skill.getUserInvocable() != null) {
+ response.put("user-invocable", skill.getUserInvocable());
+ }
+ if (skill.getDisableModelInvocation() != null) {
+ response.put("disable-model-invocation", skill.getDisableModelInvocation());
+ }
+ if (skill.getMetadata() != null && !skill.getMetadata().isEmpty()) {
+ ObjectNode meta = getMapper().createObjectNode();
+ for (Map.Entry e : skill.getMetadata().entrySet()) {
+ meta.put(e.getKey(), e.getValue());
+ }
+ response.set("metadata", meta);
+ }
+
+ Map namespaceMode = getNamespaceMode();
+ ArrayNode toolList = getMapper().createArrayNode();
+ for (SkillToolSpec tool : skill.getTools()) {
+ ObjectNode toolEntry = getMapper().createObjectNode();
+ toolEntry.put("name", tool.getName());
+ toolEntry.put("description", tool.getDescription());
+ if (tool.getFrom() != null) {
+ toolEntry.put("type", "derived");
+ ObjectNode invRef = getMapper().createObjectNode();
+ invRef.put("targetNamespace", tool.getFrom().getSourceNamespace());
+ invRef.put("action", tool.getFrom().getAction());
+ invRef.put("mode", namespaceMode.getOrDefault(tool.getFrom().getSourceNamespace(), "unknown"));
+ toolEntry.set("invocationRef", invRef);
+ } else if (tool.getInstruction() != null) {
+ toolEntry.put("type", "instruction");
+ toolEntry.put("instruction", tool.getInstruction());
+ }
+ toolList.add(toolEntry);
+ }
+ response.set("tools", toolList);
+
+ return new JacksonRepresentation<>(response);
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java b/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java
new file mode 100644
index 0000000..9baa3c5
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillDownloadResource.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.OutputRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import org.restlet.resource.ResourceException;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+
+/**
+ * Handles {@code GET /skills/{name}/download} — streams a ZIP archive of the skill's
+ * {@code location} directory.
+ *
+ *
Returns 404 if the skill is not found, has no {@code location} configured, or the location
+ * directory does not exist.
+ */
+public class SkillDownloadResource extends SkillServerResource {
+
+ @Get
+ public Representation download() {
+ String name = getAttribute("name");
+ ExposedSkillSpec skill = findSkill(name);
+ if (skill == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name);
+ }
+ if (skill.getLocation() == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "No location configured for skill: " + name);
+ }
+
+ final Path root = Paths.get(URI.create(skill.getLocation())).normalize().toAbsolutePath();
+ if (!Files.exists(root) || !Files.isDirectory(root)) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "Location directory not found for skill: " + name);
+ }
+
+ return new OutputRepresentation(MediaType.APPLICATION_ZIP) {
+ @Override
+ public void write(OutputStream os) throws IOException {
+ try (ZipOutputStream zip = new ZipOutputStream(os)) {
+ Files.walkFileTree(root, new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ String entry = root.relativize(file).toString().replace('\\', '/');
+ zip.putNextEntry(new ZipEntry(entry));
+ Files.copy(file, zip);
+ zip.closeEntry();
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java b/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java
new file mode 100644
index 0000000..9e8ae21
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillFileResource.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.representation.FileRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.resource.Get;
+import org.restlet.resource.ResourceException;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+
+/**
+ * Handles {@code GET /skills/{name}/contents/{file}} — serves an individual file from the skill's
+ * {@code location} directory.
+ *
+ *
The {@code {file}} template variable is configured with {@code TYPE_URI_PATH} in
+ * {@link SkillServerAdapter}, allowing multi-segment paths such as
+ * {@code examples/advanced-usage.md}.
+ *
+ *
Path traversal is blocked via {@link SkillServerResource#resolveAndValidate}.
+ *
+ *
+ *
Returns 400 if the requested path contains unsafe segments.
+ *
Returns 404 if the skill, location, or file is not found.
+ *
+ */
+public class SkillFileResource extends SkillServerResource {
+
+ @Get
+ public Representation getFile() {
+ String name = getAttribute("name");
+ String file = getAttribute("file");
+
+ ExposedSkillSpec skill = findSkill(name);
+ if (skill == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, "Skill not found: " + name);
+ }
+ if (skill.getLocation() == null) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "No location configured for skill: " + name);
+ }
+
+ Path resolved;
+ try {
+ resolved = resolveAndValidate(skill.getLocation(), file);
+ } catch (SecurityException e) {
+ throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, e.getMessage());
+ }
+
+ if (!Files.exists(resolved) || !Files.isRegularFile(resolved)) {
+ throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND,
+ "File not found: " + file);
+ }
+
+ MediaType mediaType = detectMediaType(resolved.getFileName().toString());
+ return new FileRepresentation(resolved.toFile(), mediaType);
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java
new file mode 100644
index 0000000..88291c0
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillServerAdapter.java
@@ -0,0 +1,168 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import org.restlet.Context;
+import org.restlet.Server;
+import org.restlet.data.Protocol;
+import org.restlet.routing.Router;
+import org.restlet.routing.TemplateRoute;
+import org.restlet.routing.Variable;
+import io.naftiko.Capability;
+import io.naftiko.spec.exposes.ApiServerSpec;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+import io.naftiko.spec.exposes.McpServerSpec;
+import io.naftiko.spec.exposes.ServerSpec;
+import io.naftiko.spec.exposes.SkillServerSpec;
+import io.naftiko.spec.exposes.SkillToolSpec;
+
+/**
+ * Skill Server Adapter — exposes a read-only catalog of agent skills over predefined HTTP
+ * endpoints.
+ *
+ *
Routes:
+ *
+ *
{@code GET /skills} — list all skills
+ *
{@code GET /skills/{name}} — skill metadata + tool catalog
+ *
{@code GET /skills/{name}/download} — ZIP archive
+ *
{@code GET /skills/{name}/contents} — file listing
+ *
{@code GET /skills/{name}/contents/{file}} — individual file
+ *
+ *
+ *
At construction time the adapter validates that every tool in every skill either references a
+ * sibling {@code api}/{@code mcp} namespace (via {@code from}) or points to an instruction file
+ * (via {@code instruction}) — never both and never neither.
+ */
+public class SkillServerAdapter extends ServerAdapter {
+
+ private final Server server;
+ private final Map namespaceMode;
+
+ public SkillServerAdapter(Capability capability, SkillServerSpec serverSpec) {
+ super(capability, serverSpec);
+
+ this.namespaceMode = buildNamespaceMode(capability, serverSpec);
+ validateSkills(serverSpec, namespaceMode);
+
+ Context context = new Context();
+ context.getAttributes().put("skillServerSpec", serverSpec);
+ context.getAttributes().put("namespaceMode", namespaceMode);
+
+ Router router = new Router(context);
+ router.attach("/skills", SkillCatalogResource.class);
+ router.attach("/skills/{name}", SkillDetailResource.class);
+ router.attach("/skills/{name}/download", SkillDownloadResource.class);
+ router.attach("/skills/{name}/contents", SkillContentsResource.class);
+ TemplateRoute fileRoute =
+ router.attach("/skills/{name}/contents/{file}", SkillFileResource.class);
+ fileRoute.getTemplate().getVariables().put("file", new Variable(Variable.TYPE_URI_PATH));
+
+ this.server = new Server(Protocol.HTTP, serverSpec.getAddress(), serverSpec.getPort());
+ this.server.setNext(router);
+ }
+
+ /**
+ * Builds a lookup map from namespace name to adapter type ({@code "api"} or {@code "mcp"})
+ * by scanning the sibling {@link ServerSpec} entries in the capability. The skill adapter
+ * itself is excluded.
+ */
+ private static Map buildNamespaceMode(Capability capability,
+ SkillServerSpec selfSpec) {
+ Map map = new HashMap<>();
+ for (ServerSpec spec : capability.getSpec().getCapability().getExposes()) {
+ if (spec == selfSpec) {
+ continue;
+ }
+ if (spec instanceof ApiServerSpec) {
+ String ns = ((ApiServerSpec) spec).getNamespace();
+ if (ns != null) {
+ map.put(ns, "api");
+ }
+ } else if (spec instanceof McpServerSpec) {
+ String ns = ((McpServerSpec) spec).getNamespace();
+ if (ns != null) {
+ map.put(ns, "mcp");
+ }
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Fail-fast validation: every tool must have exactly one of {@code from} or
+ * {@code instruction}; {@code from} namespaces must resolve to a sibling adapter;
+ * {@code instruction} tools require a skill-level {@code location}.
+ */
+ private static void validateSkills(SkillServerSpec serverSpec,
+ Map namespaceMode) {
+ for (ExposedSkillSpec skill : serverSpec.getSkills()) {
+ for (SkillToolSpec tool : skill.getTools()) {
+ boolean hasFrom = tool.getFrom() != null;
+ boolean hasInstruction = tool.getInstruction() != null;
+ if (!hasFrom && !hasInstruction) {
+ throw new IllegalArgumentException("Skill tool '" + tool.getName()
+ + "' in skill '" + skill.getName()
+ + "' must define either 'from' or 'instruction'.");
+ }
+ if (hasFrom && hasInstruction) {
+ throw new IllegalArgumentException("Skill tool '" + tool.getName()
+ + "' in skill '" + skill.getName()
+ + "' cannot define both 'from' and 'instruction'.");
+ }
+ if (hasFrom) {
+ String ns = tool.getFrom().getSourceNamespace();
+ if (!namespaceMode.containsKey(ns)) {
+ throw new IllegalArgumentException("Skill tool '" + tool.getName()
+ + "' in skill '" + skill.getName()
+ + "' references unknown namespace '" + ns
+ + "'. Expected one of: " + namespaceMode.keySet());
+ }
+ }
+ if (hasInstruction && skill.getLocation() == null) {
+ throw new IllegalArgumentException("Skill tool '" + tool.getName()
+ + "' in skill '" + skill.getName()
+ + "' uses 'instruction' but the skill has no 'location' configured.");
+ }
+ }
+ }
+ }
+
+ public SkillServerSpec getSkillServerSpec() {
+ return (SkillServerSpec) getSpec();
+ }
+
+ public Server getServer() {
+ return server;
+ }
+
+ @Override
+ public void start() throws Exception {
+ server.start();
+ Context.getCurrentLogger().log(Level.INFO,
+ "Skill Server started on " + getSkillServerSpec().getAddress() + ":"
+ + getSkillServerSpec().getPort() + " (namespace: "
+ + getSkillServerSpec().getNamespace() + ")");
+ }
+
+ @Override
+ public void stop() throws Exception {
+ server.stop();
+ Context.getCurrentLogger().log(Level.INFO,
+ "Skill Server stopped on " + getSkillServerSpec().getAddress() + ":"
+ + getSkillServerSpec().getPort());
+ }
+}
diff --git a/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java b/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java
new file mode 100644
index 0000000..4dc6ecc
--- /dev/null
+++ b/src/main/java/io/naftiko/engine/exposes/SkillServerResource.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine.exposes;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.regex.Pattern;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.restlet.data.MediaType;
+import org.restlet.resource.ServerResource;
+import org.restlet.service.MetadataService;
+import io.naftiko.spec.exposes.ExposedSkillSpec;
+import io.naftiko.spec.exposes.SkillServerSpec;
+
+/**
+ * Abstract base for all skill server handler resources.
+ *
+ *
Provides shared utilities for context attribute access, skill lookup, path traversal
+ * validation, and MIME type detection. Each subclass is instantiated per request by Restlet's
+ * routing layer, receiving dependencies from the shared {@link org.restlet.Context} that is
+ * populated by {@link SkillServerAdapter} at construction time.
+ */
+abstract class SkillServerResource extends ServerResource {
+
+ /** Allowed characters in a single path segment — no {@code ..}, no special characters. */
+ private static final Pattern SAFE_SEGMENT = Pattern.compile("^[a-zA-Z0-9._-]+$");
+
+ private static final MetadataService METADATA_SERVICE = new MetadataService();
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public ObjectMapper getMapper() {
+ return mapper;
+ }
+
+ protected SkillServerSpec getSkillServerSpec() {
+ return (SkillServerSpec) getContext().getAttributes().get("skillServerSpec");
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Map getNamespaceMode() {
+ return (Map) getContext().getAttributes().get("namespaceMode");
+ }
+
+ protected ExposedSkillSpec findSkill(String name) {
+ if (name == null) {
+ return null;
+ }
+ for (ExposedSkillSpec skill : getSkillServerSpec().getSkills()) {
+ if (name.equals(skill.getName())) {
+ return skill;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resolves {@code file} relative to the given {@code locationUri} and validates that the
+ * resolved path stays within the location root (path traversal protection).
+ *
+ * @param locationUri {@code file:///} URI of the skill's location directory
+ * @param file relative file path (e.g. {@code "README.md"} or
+ * {@code "examples/usage.md"})
+ * @return the resolved absolute path
+ * @throws SecurityException if any path segment is unsafe or the resolved path escapes the root
+ */
+ protected Path resolveAndValidate(String locationUri, String file) {
+ Path root = Paths.get(URI.create(locationUri)).normalize().toAbsolutePath();
+ Path relPath = Paths.get(file);
+ for (int i = 0; i < relPath.getNameCount(); i++) {
+ String segment = relPath.getName(i).toString();
+ if (!SAFE_SEGMENT.matcher(segment).matches()) {
+ throw new SecurityException("Unsafe path segment in request: " + segment);
+ }
+ }
+ Path resolved = root.resolve(relPath).normalize().toAbsolutePath();
+ if (!resolved.startsWith(root)) {
+ throw new SecurityException("Path traversal attempt detected");
+ }
+ return resolved;
+ }
+
+ protected MediaType detectMediaType(String filename) {
+ int dot = filename.lastIndexOf('.');
+ if (dot >= 0) {
+ String extension = filename.substring(dot + 1);
+ MediaType type = METADATA_SERVICE.getMediaType(extension);
+ if (type != null) {
+ return type;
+ }
+ }
+ return MediaType.APPLICATION_OCTET_STREAM;
+ }
+}
diff --git a/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java b/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java
new file mode 100644
index 0000000..989ca75
--- /dev/null
+++ b/src/main/java/io/naftiko/spec/exposes/ExposedSkillSpec.java
@@ -0,0 +1,160 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.spec.exposes;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A skill definition.
+ *
+ *
Supports the full Agent Skills Spec
+ * frontmatter properties. Skills describe tools from sibling {@code api} or {@code mcp} adapters,
+ * or from local file instructions — they do not execute tools themselves.
Declare instruction tools (via {@code instruction}) backed by local files
+ *
Stand alone as purely descriptive (no tools, just metadata and {@code location} files)
+ *
+ */
+public class ExposedSkillSpec {
+
+ private volatile String name;
+
+ private volatile String description;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String license;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String compatibility;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private volatile Map metadata;
+
+ @JsonProperty("allowed-tools")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String allowedTools;
+
+ @JsonProperty("argument-hint")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String argumentHint;
+
+ @JsonProperty("user-invocable")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile Boolean userInvocable;
+
+ @JsonProperty("disable-model-invocation")
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile Boolean disableModelInvocation;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String location;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private final List tools;
+
+ public ExposedSkillSpec() {
+ this.tools = new CopyOnWriteArrayList<>();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getLicense() {
+ return license;
+ }
+
+ public void setLicense(String license) {
+ this.license = license;
+ }
+
+ public String getCompatibility() {
+ return compatibility;
+ }
+
+ public void setCompatibility(String compatibility) {
+ this.compatibility = compatibility;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ public String getAllowedTools() {
+ return allowedTools;
+ }
+
+ public void setAllowedTools(String allowedTools) {
+ this.allowedTools = allowedTools;
+ }
+
+ public String getArgumentHint() {
+ return argumentHint;
+ }
+
+ public void setArgumentHint(String argumentHint) {
+ this.argumentHint = argumentHint;
+ }
+
+ public Boolean getUserInvocable() {
+ return userInvocable;
+ }
+
+ public void setUserInvocable(Boolean userInvocable) {
+ this.userInvocable = userInvocable;
+ }
+
+ public Boolean getDisableModelInvocation() {
+ return disableModelInvocation;
+ }
+
+ public void setDisableModelInvocation(Boolean disableModelInvocation) {
+ this.disableModelInvocation = disableModelInvocation;
+ }
+
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public List getTools() {
+ return tools;
+ }
+}
diff --git a/src/main/java/io/naftiko/spec/exposes/ServerSpec.java b/src/main/java/io/naftiko/spec/exposes/ServerSpec.java
index 42b1bc9..d45cda0 100644
--- a/src/main/java/io/naftiko/spec/exposes/ServerSpec.java
+++ b/src/main/java/io/naftiko/spec/exposes/ServerSpec.java
@@ -30,7 +30,8 @@
)
@JsonSubTypes({
@JsonSubTypes.Type(value = ApiServerSpec.class, name = "api"),
- @JsonSubTypes.Type(value = McpServerSpec.class, name = "mcp")
+ @JsonSubTypes.Type(value = McpServerSpec.class, name = "mcp"),
+ @JsonSubTypes.Type(value = SkillServerSpec.class, name = "skill")
})
public abstract class ServerSpec {
diff --git a/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java
new file mode 100644
index 0000000..5cce5f2
--- /dev/null
+++ b/src/main/java/io/naftiko/spec/exposes/SkillServerSpec.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.spec.exposes;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * Skill Server Specification Element.
+ *
+ *
Defines a skill catalog server that exposes agent skill metadata and supporting files over
+ * predefined GET-only endpoints. Skills declare tools derived from sibling {@code api} or
+ * {@code mcp} adapters, or defined as local file instructions. The skill server does not execute
+ * tools — AI clients invoke sibling adapters directly.
+ *
+ *
Predefined endpoints:
+ *
+ *
{@code GET /skills} — list all skills
+ *
{@code GET /skills/{name}} — skill metadata + tool catalog with invocation refs
+ *
{@code GET /skills/{name}/download} — ZIP archive from the skill's {@code location}
+ *
{@code GET /skills/{name}/contents} — file listing from the skill's {@code location}
+ *
{@code GET /skills/{name}/contents/{file}} — individual file from the skill's {@code location}
+ *
+ */
+public class SkillServerSpec extends ServerSpec {
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String namespace;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String description;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private final List skills;
+
+ public SkillServerSpec() {
+ this(null, 0, null);
+ }
+
+ public SkillServerSpec(String address, int port, String namespace) {
+ super("skill", address, port);
+ this.namespace = namespace;
+ this.skills = new CopyOnWriteArrayList<>();
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public void setNamespace(String namespace) {
+ this.namespace = namespace;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public List getSkills() {
+ return skills;
+ }
+}
diff --git a/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java
new file mode 100644
index 0000000..c5d704e
--- /dev/null
+++ b/src/main/java/io/naftiko/spec/exposes/SkillToolFromSpec.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.spec.exposes;
+
+/**
+ * Source reference for a derived skill tool.
+ *
+ *
Identifies a sibling {@code api} or {@code mcp} adapter by namespace and the specific
+ * operation name (api) or tool name (mcp) to derive.
+ */
+public class SkillToolFromSpec {
+
+ private volatile String sourceNamespace;
+ private volatile String action;
+
+ public SkillToolFromSpec() {}
+
+ public SkillToolFromSpec(String sourceNamespace, String action) {
+ this.sourceNamespace = sourceNamespace;
+ this.action = action;
+ }
+
+ public String getSourceNamespace() {
+ return sourceNamespace;
+ }
+
+ public void setSourceNamespace(String sourceNamespace) {
+ this.sourceNamespace = sourceNamespace;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+}
diff --git a/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java b/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java
new file mode 100644
index 0000000..68cbb10
--- /dev/null
+++ b/src/main/java/io/naftiko/spec/exposes/SkillToolSpec.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.spec.exposes;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * A tool declared within a skill.
+ *
+ *
Exactly one of {@code from} (derived from a sibling {@code api} or {@code mcp} adapter) or
+ * {@code instruction} (path to a local file relative to the skill's {@code location} directory)
+ * must be specified.
+ */
+public class SkillToolSpec {
+
+ private volatile String name;
+
+ private volatile String description;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile SkillToolFromSpec from;
+
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private volatile String instruction;
+
+ public SkillToolSpec() {}
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public SkillToolFromSpec getFrom() {
+ return from;
+ }
+
+ public void setFrom(SkillToolFromSpec from) {
+ this.from = from;
+ }
+
+ public String getInstruction() {
+ return instruction;
+ }
+
+ public void setInstruction(String instruction) {
+ this.instruction = instruction;
+ }
+}
diff --git a/src/main/resources/specs/agent-skills-support-proposal.md b/src/main/resources/blueprints/agent-skills-support-proposal.md
similarity index 94%
rename from src/main/resources/specs/agent-skills-support-proposal.md
rename to src/main/resources/blueprints/agent-skills-support-proposal.md
index a057e30..82873f2 100644
--- a/src/main/resources/specs/agent-skills-support-proposal.md
+++ b/src/main/resources/blueprints/agent-skills-support-proposal.md
@@ -114,19 +114,42 @@ capability:
namespace: "weather-api"
baseUri: "https://api.weather.com/v1/"
resources:
- - path: "forecast/{{location}}"
+ - name: "forecast"
+ path: "forecast/{{location}}"
+ inputParameters:
+ - name: "location"
+ in: "path"
operations:
- method: "GET"
name: "get-forecast"
+ outputParameters:
+ - name: "forecast"
+ type: "object"
+ value: "$.forecast"
- type: "http"
namespace: "geocoding-api"
baseUri: "https://geocode.example.com/"
resources:
- - path: "resolve/{{query}}"
+ - name: "resolve"
+ path: "resolve/{{query}}"
+ inputParameters:
+ - name: "query"
+ in: "path"
operations:
- method: "GET"
name: "resolve-location"
+ outputParameters:
+ - name: "coordinates"
+ type: "object"
+ value: "$.coordinates"
+ properties:
+ lat:
+ type: "number"
+ value: "$.lat"
+ lon:
+ type: "number"
+ value: "$.lon"
exposes:
# API adapter — owns tool execution via REST
@@ -139,9 +162,18 @@ capability:
operations:
- method: "GET"
name: "get-forecast"
+ inputParameters:
+ - name: "city"
+ in: "path"
+ type: "string"
+ description: "City name (e.g. 'London', 'New York')"
call: "weather-api.get-forecast"
with:
location: "{{city}}"
+ outputParameters:
+ - name: "forecast"
+ type: "object"
+ mapping: "$.forecast"
# MCP adapter — owns tool execution via MCP protocol
- type: "mcp"
@@ -152,6 +184,10 @@ capability:
tools:
- name: "resolve-and-forecast"
description: "Resolve a place name to coordinates, then fetch forecast"
+ inputParameters:
+ - name: "place"
+ type: "string"
+ description: "Place name to resolve"
steps:
- type: "call"
name: "geo"
@@ -163,10 +199,16 @@ capability:
call: "weather-api.get-forecast"
with:
location: "{{geo.coordinates.lat}},{{geo.coordinates.lon}}"
- inputParameters:
- - name: "place"
- type: "string"
- description: "Place name to resolve"
+ mappings:
+ - targetName: "location"
+ value: "$.geo.coordinates"
+ - targetName: "forecast"
+ value: "$.weather.forecast"
+ outputParameters:
+ - name: "location"
+ type: "object"
+ - name: "forecast"
+ type: "object"
# Skill adapter — metadata/catalog layer (no execution)
- type: "skill"
@@ -189,16 +231,16 @@ capability:
- name: "get-forecast"
description: "Get weather forecast for a city"
from:
- namespace: "weather-rest" # Sibling API adapter
- action: "get-forecast" # Operation name
+ sourceNamespace: "weather-rest" # Sibling API adapter
+ action: "get-forecast" # Operation name
- name: "resolve-and-forecast"
description: "Resolve a place name to coordinates, then fetch forecast"
from:
- namespace: "weather-mcp" # Sibling MCP adapter
- action: "resolve-and-forecast" # Tool name
+ sourceNamespace: "weather-mcp" # Sibling MCP adapter
+ action: "resolve-and-forecast" # Tool name
- name: "interpret-weather"
description: "Guide for reading and interpreting weather data"
- instruction: "interpret-weather.md" # Local file in location dir
+ instruction: "interpret-weather.md" # Local file in location dir
```
**How agents use this:**
@@ -259,18 +301,18 @@ skills:
- name: "list-orders"
description: "List all customer orders"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "list-orders"
- name: "create-order"
description: "Create a new customer order"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "create-order"
# Derived from sibling MCP adapter
- name: "summarize-order"
description: "Generate an AI summary of an order"
from:
- namespace: "assistant-mcp"
+ sourceNamespace: "assistant-mcp"
action: "summarize-order"
# Local file instruction
- name: "order-policies"
@@ -283,7 +325,7 @@ skills:
A tool with `from` references a specific operation or tool in a sibling adapter:
**Tool declaration rules:**
-1. `from.namespace` must resolve to a sibling `exposes[]` entry of type `api` or `mcp`
+1. `from.sourceNamespace` must resolve to a sibling `exposes[]` entry of type `api` or `mcp`
2. `from.action` must match an operation name (api) or tool name (mcp) in the resolved adapter
3. Adapter type is inferred from the resolved target
4. Each derived tool includes an `invocationRef` in the response so agents can invoke the source adapter directly
@@ -356,12 +398,12 @@ skills:
- name: "run-analysis"
description: "Run a full data analysis"
from:
- namespace: "analytics-rest"
+ sourceNamespace: "analytics-rest"
action: "run-analysis"
- name: "quick-stats"
description: "Run quick statistical analysis"
from:
- namespace: "analytics-mcp"
+ sourceNamespace: "analytics-mcp"
action: "quick-stats"
- name: "interpret-data"
description: "Guide for interpreting analysis results"
@@ -414,7 +456,7 @@ skills:
- name: "get-forecast"
description: "Get weather forecast"
from:
- namespace: "weather-rest"
+ sourceNamespace: "weather-rest"
action: "get-forecast"
- name: "interpret-weather"
description: "Guide for interpreting weather data"
@@ -494,10 +536,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as
"description": "A skill definition. Declares tools derived from sibling api or mcp adapters or defined as local file instructions. Can also stand alone as purely descriptive (no tools). Supports full Agent Skills Spec frontmatter metadata. Skills describe tools — they do not execute them.",
"properties": {
"name": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$",
- "minLength": 1,
- "maxLength": 64,
+ "$ref": "#/$defs/IdentifierKebab",
"description": "Skill identifier (kebab-case)"
},
"description": {
@@ -521,6 +560,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as
},
"allowed-tools": {
"type": "string",
+ "maxLength": 1024,
"description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)"
},
"argument-hint": {
@@ -563,10 +603,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as
"description": "A tool declared within a skill. Derived from a sibling api or mcp adapter via 'from', or defined as a local file instruction.",
"properties": {
"name": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$",
- "minLength": 1,
- "maxLength": 64,
+ "$ref": "#/$defs/IdentifierKebab",
"description": "Tool identifier (kebab-case)"
},
"description": {
@@ -577,7 +614,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as
"type": "object",
"description": "Derive this tool from a sibling api or mcp adapter.",
"properties": {
- "namespace": {
+ "sourceNamespace": {
"type": "string",
"description": "Sibling exposes[].namespace (must be type api or mcp)"
},
@@ -586,7 +623,7 @@ The SKILL.md file at the location can contain the same frontmatter properties as
"description": "Operation name (api) or tool name (mcp) in the source adapter"
}
},
- "required": ["namespace", "action"],
+ "required": ["sourceNamespace", "action"],
"additionalProperties": false
},
"instruction": {
@@ -950,6 +987,9 @@ capability:
resources:
- path: "forecast/{{location}}"
name: "forecast"
+ inputParameters:
+ - name: "location"
+ in: "path"
operations:
- method: "GET"
name: "get-forecast"
@@ -964,13 +1004,23 @@ capability:
resources:
- path: "search/{{query}}"
name: "search"
+ inputParameters:
+ - name: "query"
+ in: "path"
operations:
- method: "GET"
name: "resolve-location"
outputParameters:
- name: "coordinates"
type: "object"
- value: "$.result"
+ value: "$.coordinates"
+ properties:
+ lat:
+ type: "number"
+ value: "$.lat"
+ lon:
+ type: "number"
+ value: "$.lon"
exposes:
# API adapter — executes the forecast tool via REST
@@ -983,14 +1033,18 @@ capability:
operations:
- method: "GET"
name: "get-forecast"
- call: "weather-api.get-forecast"
- with:
- location: "{{city}}"
inputParameters:
- name: "city"
in: "path"
type: "string"
description: "City name (e.g. 'London', 'New York')"
+ call: "weather-api.get-forecast"
+ with:
+ location: "{{city}}"
+ outputParameters:
+ - name: "forecast"
+ type: "object"
+ mapping: "$.forecast"
# MCP adapter — executes multi-step tools via MCP protocol
- type: "mcp"
@@ -1049,12 +1103,12 @@ capability:
- name: "get-forecast"
description: "Get weather forecast for a city"
from:
- namespace: "weather-rest"
+ sourceNamespace: "weather-rest"
action: "get-forecast"
- name: "resolve-and-forecast"
description: "Resolve a place name to coordinates, then fetch forecast"
from:
- namespace: "weather-mcp"
+ sourceNamespace: "weather-mcp"
action: "resolve-and-forecast"
- name: "interpret-weather"
description: "Guide for reading and interpreting weather data"
@@ -1243,17 +1297,17 @@ capability:
- name: "list-orders"
description: "List all customer orders"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "list-orders"
- name: "get-order"
description: "Get details of a specific order"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "get-order"
- name: "create-order"
description: "Create a new customer order"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "create-order"
- name: "order-admin"
@@ -1263,7 +1317,7 @@ capability:
- name: "cancel-order"
description: "Cancel an existing order"
from:
- namespace: "public-api"
+ sourceNamespace: "public-api"
action: "cancel-order"
```
@@ -1380,12 +1434,12 @@ capability:
- name: "run-analysis"
description: "Run a full data analysis via REST"
from:
- namespace: "analytics-rest"
+ sourceNamespace: "analytics-rest"
action: "run-analysis"
- name: "quick-stats"
description: "Run quick statistical analysis via MCP"
from:
- namespace: "analytics-mcp"
+ sourceNamespace: "analytics-mcp"
action: "quick-stats"
- name: "analysis-methodology"
description: "Guide for choosing the right analysis approach"
@@ -1537,8 +1591,8 @@ spec:
1. `tools` is optional — a skill can be purely descriptive (metadata + `location` only, no tools)
2. Each tool must specify exactly one source: `from` (derived) or `instruction` (local file)
-3. For derived tools (`from`), `namespace` must resolve to exactly one sibling `exposes[].namespace` of type `api` or `mcp`
-4. Referencing a `skill`-type adapter from `from.namespace` is invalid (no recursive derivation)
+3. For derived tools (`from`), `sourceNamespace` must resolve to exactly one sibling `exposes[].namespace` of type `api` or `mcp`
+4. Referencing a `skill`-type adapter from `from.sourceNamespace` is invalid (no recursive derivation)
5. For derived tools, `action` must exist as an operation name (api) or tool name (mcp) in the resolved adapter
6. For instruction tools, the skill must have a `location` configured — the instruction path is resolved relative to it
7. Tool `name` values must be unique within a skill
diff --git a/src/main/resources/specs/gap-analysis-report.md b/src/main/resources/blueprints/gap-analysis-report.md
similarity index 100%
rename from src/main/resources/specs/gap-analysis-report.md
rename to src/main/resources/blueprints/gap-analysis-report.md
diff --git a/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md b/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md
new file mode 100644
index 0000000..9e1665b
--- /dev/null
+++ b/src/main/resources/blueprints/kubernetes-backstage-governance-proposal.md
@@ -0,0 +1,924 @@
+# Naftiko Fabric Governance & Operations Proposal
+## Kubernetes CRDs, Spec Metadata Taxonomy, Governance Rules, and Owned Toolchain
+
+**Status**: Proposal
+**Date**: March 9, 2026
+**Key Concept**: A coherent, end-to-end proposal for operating Naftiko capabilities as Kubernetes Custom Resources, governing them at the spec layer via a shared rules engine, and surfacing fabric-wide visibility in Backstage — all driven by small, purposeful additions to the Naftiko specification itself.
+
+---
+
+## Table of Contents
+
+1. [Executive Summary](#executive-summary)
+2. [Spec Metadata Taxonomy](#spec-metadata-taxonomy)
+3. [Kubernetes CRD and Operator](#kubernetes-crd-and-operator)
+4. [SRE Experience](#sre-experience)
+5. [Governance Rules](#governance-rules)
+6. [Backstage Integration](#backstage-integration)
+7. [Owned Toolchain](#owned-toolchain)
+8. [Shared Rules Package](#shared-rules-package)
+9. [Complete Signal Chain](#complete-signal-chain)
+10. [Summary Tables](#summary-tables)
+
+---
+
+## Executive Summary
+
+### What This Proposes
+
+Four interconnected additions that compose into a complete fabric governance story:
+
+1. **Spec Metadata Taxonomy** — Small, purposeful additions to the Naftiko spec (`labels` on `Info`, `tags` on `Consumes`/`Exposes`/`ExposedOperation`/`ConsumedHttpOperation`, `lifecycle` on `Exposes`) that serve as the single source of truth for every downstream governance tool.
+
+2. **Kubernetes Native Operations** — A `NaftikoCapability` CRD and operator that turns every spec into a running workload with generated Deployments, Services, and ExternalSecrets — with zero imperative steps from SREs. Resilience patterns (circuit breakers, retries, bulkheads, rate limiters) are provided in-process via Resilience4j — no sidecar or service mesh required.
+
+3. **Governance Rules** — A split ruleset (blocking core rules + advisory governance rules) evaluated consistently at IDE authoring time, CI merge gates, and Backstage scorecard checks — from a single shared TypeScript package.
+
+4. **Owned Toolchain** — A VS Code extension, Docker Desktop extension, and Backstage plugin suite that replace third-party dependencies (Spectral, manual catalog entries) with first-class Naftiko experiences, all powered by the same shared rules package.
+
+### Design Principle
+
+> **The Naftiko spec is the source of truth, not Kubernetes annotations or Backstage metadata.** Every label, catalog entity, risk signal, and cost allocation entry is derived from the spec — ensuring they stay in sync without extra maintenance burden on teams.
+
+---
+
+## Spec Metadata Taxonomy
+
+### The Core Distinction: `tags` vs `labels`
+
+Two metadata types serve fundamentally different tool layers and must be kept distinct from the start:
+
+| Metadata type | Format | Consumer | Purpose |
+|---|---|---|---|
+| **tags** | `string[]` | Backstage catalog search, agent discovery, risk scorecards | Human-readable semantic classifiers |
+| **labels** | `map` | K8s operator → Kubernetes labels, Kubecost aggregation, Backstage entity filtering | Machine-readable key-value selectors |
+
+This mirrors Kubernetes' own `labels`/`annotations` split intentionally — it is a pattern SREs already know.
+
+---
+
+### 2.1 `Info` Object — Add `labels`
+
+`Info` already has `tags` for capability-level discovery. The new `labels` map is what the Kubernetes operator propagates verbatim onto every generated resource (Deployment, Service, ExternalSecret). It is the single source of truth for cost allocation and label selectors.
+
+**Proposed addition to `Info` fixed fields:**
+
+| Field Name | Type | Description |
+|---|---|---|
+| **labels** | `map` | Key-value pairs propagated to all Kubernetes resources generated by the Naftiko operator. Used for cost attribution (Kubecost), Backstage entity filtering, and network policy scoping. Keys MUST follow the pattern `^[a-zA-Z0-9./\-]+$`. |
+
+**Example:**
+
+```yaml
+info:
+ label: Notion Page Creator
+ description: Creates and manages Notion pages with rich content formatting
+ tags:
+ - notion
+ - automation
+ labels:
+ naftiko.io/domain: productivity
+ naftiko.io/cost-center: platform-team
+ naftiko.io/env: production
+ naftiko.io/tier: standard
+ stakeholders:
+ - role: owner
+ fullName: Jane Doe
+ email: jane.doe@example.com
+```
+
+---
+
+### 2.2 `Consumes` Object — Add `tags`
+
+Each consumed namespace is an upstream dependency with its own risk and cost profile. Tags here are where data classification and billing signals live. Backstage risk scorecards scan these directly.
+
+**Proposed addition to `Consumes` fixed fields:**
+
+| Field Name | Type | Description |
+|---|---|---|
+| **tags** | `string[]` | Tags classifying this consumed API's provenance, billing model, data sensitivity, and reliability. Used by governance rules and Backstage scorecards. |
+
+**Recommended vocabulary:**
+
+| Category | Values |
+|---|---|
+| Provenance | `internal`, `third-party`, `partner` |
+| Billing | `free`, `free-tier`, `paid-tier`, `metered`, `quota-limited` |
+| Data sensitivity | `pii`, `financial`, `health`, `public` |
+| Reliability | `sla-99`, `sla-999`, `best-effort` |
+
+**Example:**
+
+```yaml
+consumes:
+ - type: http
+ namespace: notion
+ baseUri: https://api.notion.com
+ description: Notion public API for page management
+ tags:
+ - third-party
+ - paid-tier
+ - pii
+ resources: [...]
+```
+
+---
+
+### 2.3 `Exposes` Object — Add `tags` and `lifecycle`
+
+The exposed adapter is a published API contract. Two new fields signal network topology intent and maturity stage to both the operator and Backstage.
+
+**Proposed additions to API Expose fixed fields:**
+
+| Field Name | Type | Description |
+|---|---|---|
+| **tags** | `string[]` | Tags classifying this adapter's visibility and access characteristics. Drive network policy generation (`public` → Ingress, `internal` → ClusterIP only) and Backstage catalog grouping. |
+| **lifecycle** | `string` | Lifecycle stage of this exposed adapter. One of: `experimental`, `production`, `deprecated`. Maps directly to Backstage's entity lifecycle field. |
+
+**Recommended `tags` vocabulary:**
+
+| Category | Values |
+|---|---|
+| Visibility | `public`, `internal`, `partner` |
+| Access | `authenticated`, `write-enabled` |
+| State | (use `lifecycle` field instead) |
+
+**Rule:** An `exposes` adapter tagged `public` in the `production` namespace MUST have `authentication` declared. A governance rule enforces this.
+
+**Example:**
+
+```yaml
+exposes:
+ - type: api
+ port: 3000
+ namespace: notion-writer
+ lifecycle: production
+ tags:
+ - internal
+ - write-enabled
+ authentication:
+ type: bearer
+ token: "{{api_token}}"
+ resources: [...]
+```
+
+**Operator behavior based on `tags`:**
+
+| Tag | Generated resources |
+|---|---|
+| `public` | ClusterIP Service + Ingress |
+| `internal` | ClusterIP Service only |
+| `deprecated` | ClusterIP Service + `Deprecated: true` response header injected |
+
+---
+
+### 2.4 `ExposedOperation` Object — Add `tags`
+
+Operation-level tags are the finest-grained risk and agent-safety signal. They determine whether an agent orchestrator requires human confirmation before invoking a tool derived from this operation.
+
+**Proposed addition to `ExposedOperation` fixed fields:**
+
+| Field Name | Type | Description |
+|---|---|---|
+| **tags** | `string[]` | Tags classifying this operation's effect, access requirements, and agent invocation policy. |
+
+**Recommended vocabulary:**
+
+| Category | Values |
+|---|---|
+| Effect | `read`, `write`, `mutating`, `idempotent`, `destructive`, `delete` |
+| Access | `admin-only`, `authenticated`, `public` |
+| Agent behavior | `requires-confirmation`, `no-auto-invoke`, `safe-to-retry` |
+
+**Example:**
+
+```yaml
+operations:
+ - method: DELETE
+ label: Archive Page
+ tags:
+ - mutating
+ - destructive
+ - requires-confirmation
+ call: notion.archive-page
+```
+
+The `requires-confirmation` and `no-auto-invoke` tags are particularly important for the MCP expose path — they signal to agent orchestrators that a tool derived from this operation should not be invoked without human approval.
+
+---
+
+### 2.5 `ConsumedHttpOperation` Object — Add `tags`
+
+This is where cost visibility and retry safety live at the most granular level. Each chargeable upstream call can be tagged, feeding directly into Backstage Cost Insights analysis.
+
+**Proposed addition to `ConsumedHttpOperation` fixed fields:**
+
+| Field Name | Type | Description |
+|---|---|---|
+| **tags** | `string[]` | Tags classifying this operation's billing impact, quota contribution, and retry safety. |
+
+**Recommended vocabulary:**
+
+| Category | Values |
+|---|---|
+| Billing | `chargeable`, `metered`, `free` |
+| Quota | `quota-limited`, `rate-limited` |
+| Safety | `idempotent`, `non-idempotent`, `retry-safe` |
+
+**Example:**
+
+```yaml
+operations:
+ - name: create-page
+ method: POST
+ tags:
+ - chargeable
+ - quota-limited
+ - non-idempotent
+```
+
+---
+
+### 2.6 What Is Deliberately Left Untagged
+
+| Object | Verdict | Rationale |
+|---|---|---|
+| `ConsumedHttpResource` | No tags | Resources are structural path groupings, not independent governance units; tag at the operation level |
+| `ExposedResource` | No tags | Same — the operation is the meaningful actor |
+| `ExternalRef` | No tags | Already carries semantic meaning via its `type` (file vs runtime) and `keys` map |
+| `Person` | No tags | Governance is expressed through `role`; tags would duplicate existing semantics |
+
+---
+
+### 2.7 Metadata Taxonomy Summary
+
+```
+Naftiko Spec Object tags labels lifecycle
+──────────────────────────────────────────────────────────────
+Info ✓ (existing) ✓ (new)
+Consumes ✓ (new)
+Exposes (API adapter) ✓ (new) ✓ (new)
+ExposedOperation ✓ (new)
+ConsumedHttpOperation ✓ (new)
+```
+
+Five objects. Two new field types (`labels` as a map, `lifecycle` as an enum). `tags` extended to four objects beyond `Info`. None of these require changes to the execution engine — they are pure metadata consumed by external tooling.
+
+---
+
+## Kubernetes CRD and Operator
+
+### 3.1 CRD Design: `NaftikoCapability`
+
+The natural mapping is a `NaftikoCapability` Custom Resource whose `spec` is the Naftiko YAML document itself. The operator is the only place that knows how to materialise a spec into running infrastructure.
+
+```yaml
+apiVersion: naftiko.io/v1alpha1
+kind: Capability
+metadata:
+ name: notion-page-creator
+ namespace: integrations
+spec:
+ naftiko: "0.4"
+ info:
+ label: Notion Page Creator
+ description: Creates and manages Notion pages
+ tags: [notion, automation]
+ labels:
+ naftiko.io/cost-center: platform-team
+ naftiko.io/domain: productivity
+ stakeholders:
+ - role: owner
+ fullName: Jane Doe
+ email: jane.doe@example.com
+ capability:
+ exposes:
+ - type: api
+ port: 3000
+ namespace: notion-writer
+ lifecycle: production
+ tags: [internal]
+ authentication:
+ type: bearer
+ token: "{{api_token}}"
+ resources: [...]
+ consumes:
+ - type: http
+ namespace: notion
+ baseUri: https://api.notion.com
+ description: Notion public API
+ tags: [third-party, paid-tier, pii]
+ resources: [...]
+status:
+ phase: Running
+ endpoint: http://notion-page-creator.integrations.svc.cluster.local:3000
+ conditions:
+ - type: Ready
+ status: "True"
+ lastTransitionTime: "2026-03-08T10:00:00Z"
+ - type: SecretsSynced
+ status: "True"
+```
+
+---
+
+### 3.2 Operator Reconciliation Loop
+
+The operator watches `Capability` CRs and for each one reconciles the following resources:
+
+| Resource | Description |
+|---|---|
+| **ConfigMap** | Serializes `spec` back to YAML and mounts it as `/capability.yaml` |
+| **Deployment** | Runs `naftiko/engine:latest` mounting the ConfigMap; resource requests/limits derived from `info.labels["naftiko.io/tier"]` |
+| **Service** | ClusterIP on the declared `exposes[].port`, named matching the CR |
+| **Ingress** | Generated only when `exposes[].tags` contains `public` |
+| **ExternalSecret** | One per `externalRefs` entry, resolved via External Secrets Operator targeting Vault/AWS SM/K8s Secrets |
+| **Resilience4j config** | In-process circuit breaker, retry, bulkhead, and rate limiter configuration injected as environment variables into the engine container; parameters derived from `CapabilityClass` |
+
+**Key detail — ExternalRef bridge:** The operator detects `externalRefs` entries in the spec and creates `ExternalSecret` resources automatically. SREs never hard-code secrets in specs — they reference a secret store and the operator wires the injection. This makes `externalRefs[].type: runtime` the production-safe pattern at zero extra operator instructions.
+
+**Label propagation:** Every resource generated by the operator is stamped with all entries from `info.labels`. This means Kubecost's cost aggregation by `naftiko.io/cost-center` works with zero additional configuration.
+
+---
+
+### 3.3 Status Conditions
+
+The operator writes standard Kubernetes conditions to `.status.conditions`:
+
+| Condition | Meaning |
+|---|---|
+| `Ready` | All generated resources are healthy |
+| `SecretsSynced` | All `ExternalSecret` resources have resolved |
+| `Degraded` | An upstream `externalRefs` secret store is unreachable |
+| `CircuitOpen` | At least one consumed namespace circuit breaker is open; written when the engine reports a tripped breaker via `/metrics` |
+
+---
+
+## SRE Experience
+
+### 4.1 GitOps-First
+
+Capability CRDs live in a `capabilities/` directory in the platform GitOps repo. ArgoCD or Flux syncs them; the operator does the rest. Promotion across environments means moving a YAML file between namespace overlays (Kustomize). SREs never run `docker run` or `kubectl apply` manually.
+
+### 4.2 Observability Out-of-the-Box
+
+Every resource generated by the operator carries consistent labels from `info.labels`. The engine exposes:
+
+- `GET /health` — liveness/readiness (drives Kubernetes probes)
+- `GET /metrics` — Prometheus format: request count, latency histograms, upstream error rates per consumed namespace
+
+Structured JSON logs include `capability`, `namespace`, `operation`, `statusCode`, `durationMs` fields — feeding Prometheus and Grafana dashboards scoped per capability without any SRE configuration.
+
+### 4.3 Scaling and Resource Governance
+
+A `CapabilityClass` cluster-scoped resource (a companion CRD) defines resource tiers:
+
+```yaml
+apiVersion: naftiko.io/v1alpha1
+kind: CapabilityClass
+metadata:
+ name: standard
+spec:
+ resources:
+ requests: { memory: "256Mi", cpu: "250m" }
+ limits: { memory: "512Mi", cpu: "500m" }
+ hpa:
+ minReplicas: 1
+ maxReplicas: 4
+ targetRequestsPerSecond: 100
+```
+
+The `info.labels["naftiko.io/tier"]` value selects the class. SREs control blast radius at the class level without touching individual capability specs.
+
+### 4.4 Failure Isolation
+
+The Naftiko engine embeds [Resilience4j](https://resilience4j.readme.io/) and wraps every outbound HTTP call to a consumed namespace with a configurable resilience pipeline. Because the engine is a plain Java process identical across all capabilities, resilience runs in-process with no sidecar or service mesh required — working identically in Docker Desktop local development, CI containers, and Kubernetes.
+
+The operator derives resilience parameters from the `CapabilityClass` selected by `info.labels["naftiko.io/tier"]` and injects them into the engine container as typed environment variables:
+
+```yaml
+# CapabilityClass extended with resilience defaults
+apiVersion: naftiko.io/v1alpha1
+kind: CapabilityClass
+metadata:
+ name: standard
+spec:
+ resources:
+ requests: { memory: "256Mi", cpu: "250m" }
+ limits: { memory: "512Mi", cpu: "500m" }
+ hpa:
+ minReplicas: 1
+ maxReplicas: 4
+ targetRequestsPerSecond: 100
+ resilience:
+ circuitBreaker:
+ slidingWindowSize: 10 # calls in rolling window
+ failureRateThreshold: 50 # % failures to open circuit
+ waitDurationInOpenState: 30s
+ permittedCallsInHalfOpenState: 3
+ retry:
+ maxAttempts: 3
+ waitDuration: 500ms
+ retryOnResultPredicate: "statusCode >= 500"
+ bulkhead:
+ maxConcurrentCalls: 20 # per consumed namespace
+ maxWaitDuration: 100ms
+ rateLimit:
+ limitForPeriod: 100 # calls per refresh period
+ limitRefreshPeriod: 1s
+ timeoutDuration: 0ms # fail-fast if limit exceeded
+```
+
+The engine maps each `consumes[].namespace` to an independent Resilience4j instance, so a degraded upstream API (e.g. `notion`) trips only its own circuit breaker and does not affect the `github` namespace on the same capability.
+
+**Per-namespace override via `consumes` tags:** A `consumes` entry tagged `best-effort` receives a more aggressive circuit breaker (lower `waitDurationInOpenState`) than one tagged `sla-999`, allowing SREs to tune resilience policy at the spec layer without changing `CapabilityClass` defaults.
+
+Resilience4j emits events that the engine exposes on `GET /metrics` as Prometheus counters and gauges:
+
+| Metric | Description |
+|---|---|
+| `naftiko_circuit_breaker_state{namespace}` | 0=closed, 1=open, 2=half-open |
+| `naftiko_circuit_breaker_failure_rate{namespace}` | Rolling failure rate (%) |
+| `naftiko_retry_calls_total{namespace,result}` | Retry attempts by outcome |
+| `naftiko_bulkhead_available_slots{namespace}` | Remaining concurrent call capacity |
+| `naftiko_rate_limiter_available_permissions{namespace}` | Remaining rate limit tokens |
+
+Grafana dashboards scoped to each capability show circuit breaker state transitions as annotations, making upstream degradation immediately visible without log diving.
+
+---
+
+## Governance Rules
+
+### 5.1 Architecture
+
+Two rulesets with a clear severity contract:
+
+| Ruleset | Severity | Effect |
+|---|---|---|
+| `naftiko-core-rules` | `error` | Blocks CI merge; mirrors in Kyverno admission (cluster-level last-mile) |
+| `naftiko-governance-rules` | `warn` / `info` | Advisory only; feeds Backstage Tech Insights scorecard checks |
+
+Both are implemented in the shared `@naftiko/rules` TypeScript package (see [Section 8](#shared-rules-package)). The Spectral YAML format below is the **reference documentation** for rule intent — useful for teams writing Kyverno policies — but the execution format is the TypeScript package.
+
+---
+
+### 5.2 Core Rules (Error Severity — Blocking)
+
+#### `naftiko-exposes-require-authentication`
+Every exposed API adapter must declare `authentication`. All exposed APIs must be authenticated.
+
+- **Given**: `$.capability.exposes[*]`
+- **Check**: `authentication` field is present and truthy
+- **Kyverno mirror**: yes — enforced at admission in `production` namespace
+
+#### `naftiko-consumes-require-https`
+Consumed APIs must use HTTPS. Plain `http://` URIs are forbidden.
+
+- **Given**: `$.capability.consumes[*].baseUri`
+- **Check**: value matches `^https://`
+- **Kyverno mirror**: yes
+
+#### `naftiko-destructive-operation-no-public-expose`
+An `ExposedOperation` tagged `destructive` must not appear on an `exposes` adapter tagged `public`. Public destructive operations require explicit security review.
+
+- **Given**: operations on `public`-tagged exposes adapters
+- **Check**: operation does not have `destructive` tag
+- **Kyverno mirror**: yes
+
+#### `naftiko-version-required`
+The `naftiko` version field must be present at the root.
+
+#### `naftiko-info-description-minimum-length`
+`info.description` must be at least 30 characters. Short descriptions degrade agent discovery quality.
+
+#### `naftiko-consumes-description-required`
+Each `consumes` entry must have a `description`. Required for agent discovery and dependency tracking.
+
+#### `naftiko-exposes-lifecycle-valid`
+When `lifecycle` is present on an `exposes` adapter, it must be one of: `experimental`, `production`, `deprecated`.
+
+---
+
+### 5.3 Governance Rules (Warn/Info Severity — Advisory)
+
+#### Cost
+| Rule ID | Check | Severity |
+|---|---|---|
+| `naftiko-info-labels-required` | `info.labels` must be present | `warn` |
+| `naftiko-info-labels-cost-center` | `info.labels` must contain `naftiko.io/cost-center` | `warn` |
+| `naftiko-consumes-billing-tag` | Each `consumes` entry must have a billing tag (`free`, `free-tier`, `paid-tier`, `metered`, `quota-limited`) | `warn` |
+
+#### Risk
+| Rule ID | Check | Severity |
+|---|---|---|
+| `naftiko-info-stakeholders-required` | `info.stakeholders` must be present | `warn` |
+| `naftiko-consumes-provenance-tag` | Each `consumes` entry must have a provenance tag (`internal`, `third-party`, `partner`) | `warn` |
+| `naftiko-pii-consumes-requires-auth` | A `pii`-tagged `consumes` entry must have `authentication` on all `exposes` adapters | `warn` |
+
+#### Efficiency & Maturity
+| Rule ID | Check | Severity |
+|---|---|---|
+| `naftiko-exposes-lifecycle-required` | All `exposes` adapters should declare `lifecycle` | `warn` |
+| `naftiko-exposes-tags-required` | All `exposes` adapters should have tags | `warn` |
+| `naftiko-consumes-tags-required` | All `consumes` entries should have tags | `warn` |
+| `naftiko-info-tags-required` | `info.tags` should be present | `warn` |
+| `naftiko-deprecated-expose-has-sunset` | A `deprecated` `exposes` adapter should carry a `sunset:YYYY-MM-DD` tag | `info` |
+
+#### Agent Safety
+| Rule ID | Check | Severity |
+|---|---|---|
+| `naftiko-mutating-method-has-effect-tag` | `POST`/`PUT`/`PATCH`/`DELETE` exposed operations should declare an effect tag (`write`, `mutating`, `destructive`, `idempotent`) | `warn` |
+
+---
+
+### 5.4 Kyverno Mirror Policies
+
+Core rules have exact equivalents as Kyverno `ClusterPolicy` resources. The two layers enforce the same contracts at different times: governance rules at authoring/CI, Kyverno at cluster admission. They share the same rule vocabulary from the spec metadata taxonomy — the source of truth.
+
+Example for `naftiko-exposes-require-authentication`:
+
+```yaml
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: naftiko-exposes-require-authentication
+spec:
+ validationFailureAction: Enforce
+ rules:
+ - name: check-exposes-authentication
+ match:
+ any:
+ - resources:
+ kinds: [Capability]
+ namespaces: [production]
+ validate:
+ message: "All exposes adapters in production must declare authentication."
+ foreach:
+ - list: "request.object.spec.capability.exposes"
+ deny:
+ conditions:
+ any:
+ - key: "{{ element.authentication }}"
+ operator: Equals
+ value: ""
+```
+
+---
+
+## Backstage Integration
+
+### 6.1 Fabric-Level Governance Pillars
+
+Backstage governs the three pillars derived from spec metadata:
+
+| Pillar | Source signals | Tooling |
+|---|---|---|
+| **Cost** | `info.labels["naftiko.io/cost-center"]`, `consumes[].tags` billing category, Kubecost aggregation by labels | Cost Insights plugin |
+| **Risk** | `info.stakeholders`, `consumes[].tags` data sensitivity, `exposes[].authentication` presence | Tech Insights scorecards |
+| **Efficiency** | `exposes[].lifecycle`, `info.description` length, `ExposedOperation.tags` agent safety | Tech Insights scorecards |
+
+---
+
+### 6.2 Auto-Population from Kubernetes CRDs
+
+The `NaftikoCapabilityEntityProvider` (backend plugin) watches the K8s API for `naftiko.io/v1alpha1/Capability` CRDs across all namespaces and synthesizes Backstage `Component` entities of `spec.type: capability`:
+
+```yaml
+# Auto-generated Backstage entity
+apiVersion: backstage.io/v1alpha1
+kind: Component
+metadata:
+ name: notion-page-creator
+ annotations:
+ naftiko.io/capability-ref: integrations/notion-page-creator
+ backstage.io/kubernetes-label-selector: naftiko.io/capability=notion-page-creator
+ naftiko.io/endpoint: http://notion-page-creator.integrations.svc:3000
+ tags: [notion, automation]
+spec:
+ type: capability
+ lifecycle: production # derived from exposes[0].lifecycle
+ owner: platform-team # derived from info.labels["naftiko.io/cost-center"]
+ consumesApis:
+ - notion-api # derived from consumes[].namespace
+ providesApis:
+ - notion-writer # derived from exposes[].namespace
+ dependsOn:
+ - resource:default/notion-api-secret
+```
+
+The `consumesApis` and `providesApis` relations let Backstage render the **fabric topology graph** automatically — showing which capabilities depend on which upstream APIs and which downstream clients consume them.
+
+Two discovery modes are supported:
+- **Kubernetes mode** (production): watches CRDs across all namespaces
+- **Git mode** (simpler setups / monorepos): scans configured repositories for `*.capability.yaml` files
+
+Both modes produce identical entity shapes.
+
+---
+
+### 6.3 Tech Insights Scorecard Checks
+
+The `NaftikoGovernanceFactRetriever` runs the `@naftiko/rules` governance rules against the spec from Git and maps findings to scorecard check facts:
+
+| Pillar | Scorecard check | Source rule |
+|---|---|---|
+| Cost | Cost center labeled | `naftiko-info-labels-cost-center` |
+| Cost | Dependency billing declared | `naftiko-consumes-billing-tag` |
+| Risk | Owner declared | `naftiko-info-stakeholders-required` |
+| Risk | PII surfaces protected | `naftiko-pii-consumes-requires-auth` |
+| Risk | Dependencies tagged with provenance | `naftiko-consumes-provenance-tag` |
+| Efficiency | Lifecycle stage declared | `naftiko-exposes-lifecycle-required` |
+| Efficiency | Agent safety tags on mutating ops | `naftiko-mutating-method-has-effect-tag` |
+| Efficiency | Description meets minimum length | `naftiko-info-description-minimum-length` |
+
+Clicking a failing check in the Backstage UI opens the spec file in GitHub at the responsible field.
+
+---
+
+### 6.4 Fabric Explorer
+
+A top-level `NaftikoFabricExplorerPage` renders the entire fabric as an interactive dependency graph. Nodes are sized by consumer count. Edges represent consume/expose relationships from entity relations.
+
+Filter controls:
+- By `lifecycle` (hide `deprecated`, focus `production`)
+- By `tags` (e.g. show only capabilities with `pii` in their consumes)
+- By `info.labels` (filter to a cost-center or domain)
+
+**Incident use case**: During an upstream API outage, selecting an API node immediately shows every capability in the fabric that depends on it and the downstream clients they serve.
+
+---
+
+### 6.5 Duplicate Detection
+
+The entity provider flags two capabilities where `consumes[].baseUri` entries overlap significantly and `exposes` serve similar paths. Surfaced as a "potential consolidation" insight — the efficiency signal that prevents API sprawl.
+
+---
+
+## Owned Toolchain
+
+The three extensions form a coherent lifecycle story, with no mandatory third-party extension dependencies:
+
+```
+VS Code Extension Docker Desktop Extension Backstage Plugins
+───────────────── ──────────────────────── ─────────────────
+Author → Validate Run → Debug → Inspect Discover → Govern → Track
+ (spec layer) (runtime layer) (fabric layer)
+```
+
+---
+
+### 7.1 VS Code Extension (`naftiko.vscode-naftiko`)
+
+The authoring companion. All governance feedback happens here before any CI or cluster involvement.
+
+**Schema-native YAML authoring**
+
+The extension registers as the language server for `*.capability.yaml` files and any YAML with a `naftiko:` root key:
+- Full IntelliSense based on `capability-schema.json` — field completion, enum values, pattern hints
+- Hover documentation: shows the spec description, rules, and spec section link for every field
+- Friendly error messages (not raw JSON Schema errors)
+
+**Inline governance diagnostics (no external tools required)**
+
+Governance and core rules from `@naftiko/rules` evaluate on every document change (debounced):
+- `error` severity → red underlines (blocking: missing auth, plain HTTP, etc.)
+- `warn` severity → yellow underlines (governance gaps: missing labels, no lifecycle, etc.)
+- `info` severity → blue underlines (suggestions: sunset date on deprecated adapters, etc.)
+
+Each diagnostic includes the rule ID (e.g. `naftiko-exposes-require-authentication`) for traceability to CI and Backstage scorecard checks, plus a **Quick Fix** code action where the fix is deterministic.
+
+**Capability topology panel**
+
+A webview panel (`Naftiko: Show Topology`) renders the consume/expose graph of the current spec as a live-updating diagram:
+- `consumes` entries as source nodes (color-coded by provenance tag: `third-party` = amber, `internal` = blue)
+- `exposes` adapters as sink nodes (with `lifecycle` badge)
+- Edges labeled with `namespace` routing
+- Clicking a node navigates to the relevant YAML section
+
+**ExternalRef resolution preview**
+
+When `externalRefs` uses `type: file`, declared variable values are shown inline as ghost text — the same values the runtime will have. Closes the feedback loop for debugging Mustache template expressions without running anything.
+
+**Run / Stop capability**
+
+A status bar button and command palette entry (`Naftiko: Run Capability`):
+1. Checks Docker is running; prompts to open Docker Desktop if not
+2. Detects `externalRefs` and prompts for unresolved runtime variables
+3. Starts the `naftiko/engine` container with correct port mapping from `exposes[].port`
+4. Streams logs into a dedicated Output Channel
+5. Shows a clickable endpoint URL in the status bar once ready
+
+**Snippet library**
+
+Built-in snippets for common patterns — `naftiko-simple-op`, `naftiko-orchestrated-op`, `naftiko-forward`, `naftiko-pii-consumes`, `naftiko-mcp-expose` — with placeholders guiding the author through required fields in order.
+
+---
+
+### 7.2 Docker Desktop Extension (`naftiko/docker-extension`)
+
+The runtime companion. Makes local capability operation feel like a first-class experience.
+
+**Fabric dashboard**
+
+All running Naftiko capability containers displayed as a tile grid. Each tile shows:
+- Capability `info.label`
+- `lifecycle` badge (`experimental` / `production` / `deprecated`)
+- Health indicator (polled from `/health`)
+- Exposed ports as clickable links
+- Uptime and restart count
+
+**Spec viewer**
+
+Clicking a tile renders the full capability topology graph (shared web component with the VS Code extension). Teams can inspect what a running container consumes and exposes without reading the YAML.
+
+**Log streaming and filtering**
+
+Per-capability structured log viewer with filter controls for:
+- Log level
+- Consumed namespace (filters to logs for a specific upstream)
+- Operation name
+
+Powered by the structured JSON logs emitted by the Naftiko engine (`capability`, `namespace`, `operation`, `statusCode`, `durationMs` fields).
+
+**ExternalRef secret injection wizard**
+
+For specs with `externalRefs[].type: runtime` entries, a form pre-populated with the declared `keys` lets developers enter values injected as environment variables into the container. Values are stored in Docker Desktop's secret store — not in the spec.
+
+**Multi-capability compose generation**
+
+An "Add to Compose" action generates a `docker-compose.yml` fragment for the selected capability, including volume mount, port mapping, and environment variable stubs.
+
+---
+
+### 7.3 Backstage Plugins
+
+Two packages following Backstage's frontend/backend convention.
+
+#### `@naftiko/backstage-plugin-backend`
+
+**`NaftikoCapabilityEntityProvider`**
+Synthesizes Backstage `Component` entities from Kubernetes CRDs (Kubernetes mode) or Git files (Git mode). Both modes produce identical entity shapes — the rest of the system is discovery-mode agnostic.
+
+**`NaftikoGovernanceFactRetriever`**
+Runs `@naftiko/rules` against the spec from Git for each `capability` entity. Produces boolean/numeric facts keyed by rule ID, stored in the Tech Insights facts store. Runs on schedule or webhook trigger.
+
+**Pre-built scorecard check definitions**
+The backend plugin registers `CheckDefinition` entries for all governance rules against the three pillars. Platform teams can extend or override checks without touching plugin code.
+
+#### `@naftiko/backstage-plugin`
+
+**`NaftikoCapabilityCard`**
+Entity page card rendering:
+- Topology graph (shared web component)
+- `lifecycle` badge with deprecation banner
+- Stakeholders list with role badges
+- `tags` as chips (color-coded: `third-party` = amber, `pii` = red)
+
+**`NaftikoScorecardCard`**
+Three-pillar scorecard (Cost / Risk / Efficiency) from Tech Insights facts, displayed as score gauges with expandable failing check lists.
+
+**`NaftikoFabricExplorerPage`**
+Top-level fabric dependency graph page (see [Section 6.4](#64-fabric-explorer)).
+
+---
+
+## Shared Rules Package
+
+### 8.1 Package Structure
+
+All governance rule logic lives in a single, framework-agnostic TypeScript package:
+
+```
+@naftiko/rules
+ src/
+ core-rules.ts ← error-severity rules (blocking)
+ governance-rules.ts ← warn/info-severity rules (advisory)
+ evaluate.ts ← runs rules against a parsed spec, returns Finding[]
+ types.ts ← Finding, Severity, Rule types
+ dist/
+ index.js ← CommonJS build for Node.js consumers
+ index.esm.js ← ESM build for browser consumers (VS Code webview, Docker Desktop)
+```
+
+### 8.2 Finding Type
+
+```typescript
+interface Finding {
+ ruleId: string; // e.g. "naftiko-exposes-require-authentication"
+ severity: "error" | "warn" | "info";
+ message: string;
+ path: string; // JSONPath to the violating node, e.g. "$.capability.exposes[0]"
+ // Optional: character range for IDE inline rendering
+ range?: { start: number; end: number };
+}
+```
+
+### 8.3 Consumers
+
+| Consumer | Usage |
+|---|---|
+| VS Code extension | `evaluate()` on document change → inline diagnostics with Quick Fix |
+| `naftiko-cli lint` | `evaluate()` in CI → exit 1 on error-severity findings |
+| Backstage backend plugin | `evaluate()` in `FactRetriever` → boolean/numeric facts per rule |
+| Docker Desktop extension | `evaluate()` on spec load → governance badge on tile |
+
+The Spectral YAML ruleset format (used in earlier drafts of this proposal) is replaced entirely by this package. It served as useful reference documentation but the execution format is now TypeScript — versioned, tested, and controlled by the Naftiko project.
+
+---
+
+## Complete Signal Chain
+
+```
+Developer opens *.capability.yaml in VS Code
+ │
+ ▼
+@naftiko/rules evaluates on change
+ → inline error/warn/info diagnostics (no external tools required)
+ → topology panel updates live
+ → ExternalRef ghost text preview
+ │
+ ▼
+Developer presses "Naftiko: Run Capability"
+ → VS Code extension starts naftiko/engine container
+ → Docker Desktop extension picks it up in the fabric dashboard
+ → Spec viewer renders topology graph
+ → Logs stream to VS Code output channel
+ │
+ ▼
+Developer pushes → CI: naftiko-cli lint
+ → @naftiko/rules evaluates, exit 1 on error findings
+ → warn findings annotate the PR (no merge block)
+ │
+ ▼
+ArgoCD/Flux syncs CRD to cluster
+ │
+ ▼
+Kyverno validates at admission (mirrors core rules)
+ → last-mile enforcement for anything that bypassed CI
+ │
+ ▼
+Naftiko operator reconciles:
+ ├── ConfigMap (spec YAML)
+ ├── Deployment (naftiko/engine, resources from CapabilityClass)
+ ├── Service (ClusterIP; +Ingress if tags contains "public")
+ ├── ExternalSecret (ESO → Vault / AWS SM / K8s Secrets)
+ ├── Resilience4j config injected as env vars (circuit breaker, retry, bulkhead, rate limiter)
+ └── Status conditions (Ready, SecretsSynced, Degraded)
+ │
+ ▼
+Prometheus scrapes /metrics → Grafana dashboards per capability
+ (includes naftiko_circuit_breaker_state, naftiko_retry_calls_total, etc.)
+Kubecost aggregates by info.labels (cost-center, domain, tier)
+ │
+ ▼
+Backstage NaftikoCapabilityEntityProvider syncs CRDs → Catalog
+ ├── consumesApis / providesApis relations → topology graph
+ ├── NaftikoGovernanceFactRetriever runs @naftiko/rules against spec from Git
+ │ → facts stored in Tech Insights store
+ ├── NaftikoScorecardCard: Cost / Risk / Efficiency pillar scores
+ ├── NaftikoCapabilityCard: topology + stakeholders + tags
+ └── NaftikoFabricExplorerPage: fabric dependency graph + incident impact
+```
+
+---
+
+## Summary Tables
+
+### Spec Changes Required
+
+| Field | Object | Type | New? | Purpose |
+|---|---|---|---|---|
+| `labels` | `Info` | `map` | Yes | K8s label propagation, Kubecost cost allocation |
+| `labels["naftiko.io/tier"]` | `Info` | `string` | Yes | Selects `CapabilityClass` for resource limits and Resilience4j defaults |
+| `tags` | `Consumes` | `string[]` | Yes | Provenance, billing, data sensitivity classification |
+| `tags` (`best-effort`, `sla-999`) | `Consumes` | `string[]` | Yes | Selects Resilience4j circuit breaker aggressiveness for a specific consumed namespace |
+| `tags` | `Exposes` (API) | `string[]` | Yes | Network topology intent, Backstage grouping |
+| `lifecycle` | `Exposes` (API) | `string` (enum) | Yes | Backstage lifecycle, operator Ingress decision |
+| `tags` | `ExposedOperation` | `string[]` | Yes | Agent safety policy, risk scoring |
+| `tags` | `ConsumedHttpOperation` | `string[]` | Yes | Billing granularity, retry safety |
+
+### New Artifacts Required
+
+| Artifact | Type | Purpose |
+|---|---|---|
+| `@naftiko/rules` | npm package | Shared governance evaluation engine |
+| `naftiko-cli` | npm package | CI wrapper around `@naftiko/rules` |
+| `naftiko.io/v1alpha1/Capability` | Kubernetes CRD | Spec-native K8s resource |
+| `naftiko.io/v1alpha1/CapabilityClass` | Kubernetes CRD | Resource tier governance + Resilience4j defaults |
+| Naftiko Operator | Kubernetes controller | Reconciles CRDs into running workloads |
+| Resilience4j (embedded in engine) | Java library | In-process circuit breaker, retry, bulkhead, rate limiter per consumed namespace |
+| `naftiko.vscode-naftiko` | VS Code extension | Authoring + inline governance + run |
+| `naftiko/docker-extension` | Docker Desktop extension | Runtime dashboard + log streaming |
+| `@naftiko/backstage-plugin-backend` | Backstage plugin | Entity provider + fact retriever |
+| `@naftiko/backstage-plugin` | Backstage plugin | Capability card + scorecard + fabric explorer |
+
+### Governance Signal Source Matrix
+
+| Tool | Evaluates | Source | When |
+|---|---|---|---|
+| VS Code extension | `@naftiko/rules` | Open document | On every change |
+| `naftiko-cli lint` | `@naftiko/rules` | Committed file | CI / pre-commit |
+| Kyverno | Mirror of core rules | CRD at admission | K8s apply time |
+| Backstage FactRetriever | `@naftiko/rules` | Spec from Git | Scheduled / webhook |
+| Docker Desktop ext. | `@naftiko/rules` | Mounted spec file | Container start |
diff --git a/src/main/resources/specs/mcp-resources-prompts-proposal.md b/src/main/resources/blueprints/mcp-resources-prompts-proposal.md
similarity index 100%
rename from src/main/resources/specs/mcp-resources-prompts-proposal.md
rename to src/main/resources/blueprints/mcp-resources-prompts-proposal.md
diff --git a/src/main/resources/schemas/capability-schema.json b/src/main/resources/schemas/capability-schema.json
index befa449..06571d5 100644
--- a/src/main/resources/schemas/capability-schema.json
+++ b/src/main/resources/schemas/capability-schema.json
@@ -671,6 +671,9 @@
},
{
"$ref": "#/$defs/ExposesMcp"
+ },
+ {
+ "$ref": "#/$defs/ExposesSkill"
}
]
},
@@ -1855,6 +1858,173 @@
"type"
],
"additionalProperties": false
+ },
+ "ExposesSkill": {
+ "type": "object",
+ "description": "Skill server adapter — metadata and catalog layer. Skills declare tools derived from sibling api or mcp adapters or defined as local file instructions. Does not execute tools.",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "skill"
+ },
+ "address": {
+ "$ref": "#/$defs/Address"
+ },
+ "port": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "namespace": {
+ "$ref": "#/$defs/IdentifierKebab",
+ "description": "Unique identifier for this skill server"
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of this skill server's purpose"
+ },
+ "authentication": {
+ "$ref": "#/$defs/Authentication"
+ },
+ "skills": {
+ "type": "array",
+ "description": "Array of skill definitions. Each skill declares tools from sibling adapters or local file instructions, or stands alone as purely descriptive.",
+ "items": {
+ "$ref": "#/$defs/ExposedSkill"
+ },
+ "minItems": 1
+ }
+ },
+ "required": [
+ "type",
+ "port",
+ "namespace",
+ "skills"
+ ],
+ "additionalProperties": false
+ },
+ "ExposedSkill": {
+ "type": "object",
+ "description": "A skill definition with full Agent Skills Spec frontmatter. Declares tools derived from sibling api or mcp adapters or defined as local file instructions, or stands alone as purely descriptive (no tools).",
+ "properties": {
+ "name": {
+ "$ref": "#/$defs/IdentifierKebab",
+ "description": "Skill identifier (kebab-case, 1-64 chars)"
+ },
+ "description": {
+ "type": "string",
+ "maxLength": 1024,
+ "description": "What the skill does and when to use it. Used for agent discovery."
+ },
+ "license": {
+ "type": "string",
+ "description": "License identifier (e.g., MIT, Apache-2.0)"
+ },
+ "compatibility": {
+ "type": "string",
+ "maxLength": 500,
+ "description": "Compatibility notes and requirements"
+ },
+ "metadata": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "Arbitrary key-value metadata (author, category, tags, ecosystem, etc.)"
+ },
+ "allowed-tools": {
+ "type": "string",
+ "maxLength": 1024,
+ "description": "Space-delimited list of pre-approved tool names (Agent Skills Spec)"
+ },
+ "argument-hint": {
+ "type": "string",
+ "description": "Hint text shown when agents invoke this skill via slash command (Agent Skills Spec)"
+ },
+ "user-invocable": {
+ "type": "boolean",
+ "default": true,
+ "description": "Whether agents can invoke this skill as a slash command (Agent Skills Spec)"
+ },
+ "disable-model-invocation": {
+ "type": "boolean",
+ "default": false,
+ "description": "Whether to prevent auto-loading this skill based on context (Agent Skills Spec)"
+ },
+ "location": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^file:///",
+ "description": "file:/// URI to a directory containing SKILL.md and supporting files. Required when any tool uses 'instruction'."
+ },
+ "tools": {
+ "type": "array",
+ "description": "Tools declared by this skill. Each tool is derived from a sibling adapter or is a local file instruction.",
+ "items": {
+ "$ref": "#/$defs/SkillTool"
+ },
+ "minItems": 1
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ],
+ "additionalProperties": false
+ },
+ "SkillTool": {
+ "type": "object",
+ "description": "A tool declared within a skill. Exactly one of 'from' (derived from a sibling api or mcp adapter) or 'instruction' (local file path relative to the skill's location) must be specified.",
+ "properties": {
+ "name": {
+ "$ref": "#/$defs/IdentifierKebab",
+ "description": "Tool identifier (kebab-case)"
+ },
+ "description": {
+ "type": "string",
+ "description": "What the tool does. Used for agent discovery."
+ },
+ "from": {
+ "type": "object",
+ "description": "Derive this tool from a sibling api or mcp adapter.",
+ "properties": {
+ "sourceNamespace": {
+ "type": "string",
+ "description": "Namespace of a sibling exposes[] entry of type api or mcp"
+ },
+ "action": {
+ "type": "string",
+ "description": "Operation name (api) or tool name (mcp) in the source adapter"
+ }
+ },
+ "required": [
+ "sourceNamespace",
+ "action"
+ ],
+ "additionalProperties": false
+ },
+ "instruction": {
+ "type": "string",
+ "description": "File path relative to the skill's location directory containing the tool instruction content"
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ],
+ "oneOf": [
+ {
+ "required": [
+ "from"
+ ]
+ },
+ {
+ "required": [
+ "instruction"
+ ]
+ }
+ ],
+ "additionalProperties": false
}
}
}
\ No newline at end of file
diff --git a/src/main/resources/schemas/examples/skill-adapter.yml b/src/main/resources/schemas/examples/skill-adapter.yml
new file mode 100644
index 0000000..9298719
--- /dev/null
+++ b/src/main/resources/schemas/examples/skill-adapter.yml
@@ -0,0 +1,117 @@
+# yaml-language-server: $schema=../capability-schema.json
+---
+naftiko: "0.5"
+info:
+ label: "Weather Forecast Capability"
+ description: "Exposes weather data via API, MCP, and skill catalog adapters"
+
+capability:
+ exposes:
+ - type: api
+ port: 3000
+ namespace: weather-api
+ resources:
+ - path: /weather/current
+ description: "Current weather conditions"
+ operations:
+ - name: get-current
+ method: GET
+ call: open-meteo.get-current
+ outputParameters:
+ - type: object
+ mapping: $.current
+ properties:
+ temperature: { type: number, mapping: $.temperature_2m }
+ windspeed: { type: number, mapping: $.windspeed_10m }
+ weathercode: { type: number, mapping: $.weathercode }
+
+ - path: /weather/alerts
+ description: "Active weather alerts"
+ operations:
+ - name: list-alerts
+ method: GET
+ call: open-meteo.list-alerts
+ outputParameters:
+ - type: array
+ mapping: $.alerts
+ items:
+ - type: object
+ properties:
+ title: { type: string, mapping: $.title }
+ severity: { type: string, mapping: $.severity }
+
+ - type: mcp
+ port: 3001
+ namespace: weather-mcp
+ description: "Weather tools for AI agents"
+ tools:
+ - name: get-current-weather
+ description: "Retrieve current weather conditions for a location"
+ inputParameters:
+ - name: location
+ type: string
+ description: "City name or coordinates (lat,lon)"
+ call: open-meteo.get-current
+ with:
+ location: "$this.weather-mcp.location"
+ outputParameters:
+ - type: object
+ mapping: $.current
+ properties:
+ temperature: { type: number, mapping: $.temperature_2m }
+ windspeed: { type: number, mapping: $.windspeed_10m }
+ weathercode: { type: number, mapping: $.weathercode }
+
+ - type: skill
+ port: 4000
+ namespace: weather-skills
+ description: "Weather forecast and climate analysis skill catalog"
+ skills:
+ - name: weather-forecast
+ description: "Real-time weather data and forecasting"
+ license: Apache-2.0
+ compatibility: "claude-3-5-sonnet,gpt-4o"
+ argument-hint: "Use when the user asks about weather, forecast, temperature, or climate"
+ location: "file:///opt/skills/weather-forecast"
+ tools:
+ - name: current-conditions
+ description: "Get current weather conditions for a specific location"
+ from:
+ sourceNamespace: weather-api
+ action: get-current
+ - name: climate-guide
+ description: "Reference guide for interpreting climate data and weather patterns"
+ instruction: "climate-interpretation-guide.md"
+
+ - name: alert-monitoring
+ description: "Severe weather alerts and monitoring guidance"
+ argument-hint: "Use when the user needs to check or monitor weather alerts"
+ location: "file:///opt/skills/alert-monitoring"
+ tools:
+ - name: active-alerts
+ description: "List active severe weather alerts for a region"
+ from:
+ sourceNamespace: weather-mcp
+ action: get-current-weather
+ - name: alert-response-guide
+ description: "Guide for responding to severe weather alerts"
+ instruction: "alert-response-guide.md"
+
+ consumes:
+ - type: http
+ namespace: open-meteo
+ baseUri: "https://api.open-meteo.com/v1/"
+ resources:
+ - name: forecast
+ path: forecast
+ operations:
+ - name: get-current
+ method: GET
+ label: "Get Current Weather"
+
+ - name: alerts
+ path: alerts
+ operations:
+ - name: list-alerts
+ method: GET
+ label: "List Active Alerts"
diff --git a/src/main/resources/specs/naftiko-specification-v0.4.md b/src/main/resources/specs/naftiko-specification-v0.4.md
deleted file mode 100644
index c490040..0000000
--- a/src/main/resources/specs/naftiko-specification-v0.4.md
+++ /dev/null
@@ -1,2112 +0,0 @@
-# Naftiko Specification
-
-**Version 0.4**
-
-**Publication Date:** February 2026
-
----
-
-- **Table of Contents**
-
-## 1. Introduction
-
-The Naftiko Specification defines a standard, language-agnostic interface for describing modular, composable capabilities. In short, a **capability** is a functional unit that consumes external APIs (sources) and exposes adapters that allow other systems to interact with it.
-
-A Naftiko capability focuses on declaring the **integration intent** — what a system needs to consume and what it exposes — rather than implementation details. This higher-level abstraction makes capabilities naturally suitable for AI-driven discovery, orchestration and integration use cases, and beyond. When properly defined, a capability can be discovered, orchestrated, validated and executed with minimal implementation logic. The specification enables description of:
-
-- **Consumed sources**: External APIs or services that the capability uses
-- **Exposed adapters**: Server interfaces that the capability provides (HTTP, REST, etc.)
-- **Orchestration**: How calls to consumed sources are combined and mapped to realize exposed functions
-- **External references**: Variables and resources resolved from external sources
-
-### 1.1 Schema Access
-
-The JSON Schema for the Naftiko Specification is available in two forms:
-
-- **Raw file** — The schema source file is hosted on GitHub: [capability-schema.json](https://github.com/naftiko/framework/blob/main/src/main/resources/schemas/capability-schema.json)
-- **Interactive viewer** — A human-friendly viewer is available at: [Schema Viewer](https://naftiko.github.io/schema-viewer/)
-
-### 1.2 Core Objects
-
-**Capability**: The central object that defines a modular functional unit with clear input/output contracts.
-
-**Consumes**: External sources (APIs, services) that the capability uses to realize its operations.
-
-**Exposes**: Server adapters that provide access to the capability's operations.
-
-**Resources**: API endpoints that group related operations.
-
-**Operations**: Individual HTTP operations (GET, POST, etc.) that can be performed on resources.
-
-**Namespace**: A unique identifier for consumed sources, used for routing and mapping with the expose layer.
-
-**ExternalRef**: A declaration of an external reference providing variables to the capability. Two variants: file-resolved (for development) and runtime-resolved (for production). Variables are explicitly declared via a `keys` map.
-
-### 1.3 Related Specifications.
-
-
-
-Three specifications that work better together.
-
-| | **OpenAPI** | **Arazzo** | **OpenCollections** | **Naftiko** |
-| --- | --- | --- | --- | --- |
-| **Focus** | Defines *what* your API is — the contract, the schema, the structure. | Defines *how* API calls are sequenced — the workflows between endpoints. | Defines *how* to use your API — the scenarios, the runnable collections. | Defines *what* a capability consumes and exposes — the integration intent. |
-| **Scope** | Single API surface | Workflows across one or more APIs | Runnable collections of API calls | Modular capability spanning multiple APIs |
-| **Key strengths** | ✓ Endpoints & HTTP methods ✓ Request/response schemas ✓ Authentication requirements ✓ Data types & validation ✓ SDK & docs generation | ✓ Multi-step sequences ✓ Step dependencies & data flow ✓ Success/failure criteria ✓ Reusable workflow definitions | ✓ Runnable, shareable collections ✓ Pre-request scripts & tests ✓ Environment variables ✓ Living, executable docs | ✓ Consume/expose duality ✓ Namespace-based routing ✓ Orchestration & forwarding ✓ AI-driven discovery ✓ Composable capabilities |
-| **Analogy** | The *parts list* and dimensions | The *assembly sequence* between parts | The *step-by-step assembly guide* you can run | The *product blueprint* — what goes in, what comes out |
-| **Best used when you need to…** | Define & document an API contract, generate SDKs, validate payloads | Describe multi-step API workflows with dependencies | Share runnable API examples, test workflows, onboard developers | Declare a composable capability that consumes sources and exposes unified interfaces |
-
-**OpenAPI** tells you the shape of the door. **Arazzo** describes the sequence of doors to walk through. **OpenCollections** lets you actually walk through them. **Naftiko** combines the features of those 3 specs into a single, coherent spec, reducing complexity and offering consistent tooling out of the box
-
----
-
-## 2. Format
-
-Naftiko specifications can be represented in YAML format, complying with the provided Naftiko schema which is made available in both JSON Schema and [JSON Structure](https://json-structure.org/) formats.
-
-All field names in the specification are **case-sensitive**.
-
-Naftiko Objects expose two types of fields:
-
-- **Fixed fields**: which have a declared name
-- **Patterned fields**: which have a declared pattern for the field name
-
----
-
-## 3. Objects and Fields
-
-### 3.1 Naftiko Object
-
-This is the root object of the Naftiko document.
-
-#### 3.1.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **naftiko** | `string` | **REQUIRED**. Version of the Naftiko schema. MUST be `"0.4"` for this version. |
-| **info** | `Info` | **REQUIRED**. Metadata about the capability. |
-| **capability** | `Capability` | **REQUIRED**. Technical configuration of the capability including sources and adapters. |
-| **externalRefs** | `ExternalRef[]` | List of external references for variable injection. Each entry declares injected variables via a `keys` map. |
-
-#### 3.1.2 Rules
-
-- The `naftiko` field MUST be present and MUST have the value `"0.4"` for documents conforming to this version of the specification.
-- Both `info` and `capability` objects MUST be present.
-- The `externalRefs` field is OPTIONAL. When present, it MUST contain at least one entry.
-- No additional properties are allowed at the root level.
-
----
-
-### 3.2 Info Object
-
-Provides metadata about the capability.
-
-#### 3.2.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **label** | `string` | **REQUIRED**. The display name of the capability. |
-| **description** | `string` | **REQUIRED**. A description of the capability. The more meaningful it is, the easier for agent discovery. |
-| **tags** | `string[]` | List of tags to help categorize the capability for discovery and filtering. |
-| **created** | `string` | Date the capability was created (format: `YYYY-MM-DD`). |
-| **modified** | `string` | Date the capability was last modified (format: `YYYY-MM-DD`). |
-| **stakeholders** | `Person[]` | List of stakeholders related to this capability (for discovery and filtering). |
-
-#### 3.2.2 Rules
-
-- Both `label` and `description` are mandatory.
-- No additional properties are allowed.
-
-#### 3.2.3 Info Object Example
-
-```yaml
-info:
- label: Notion Page Creator
- description: Creates and manages Notion pages with rich content formatting
- tags:
- - notion
- - automation
- created: "2026-02-17"
- modified: "2026-02-17"
- stakeholders:
- - role: owner
- fullName: "Jane Doe"
- email: "jane.doe@example.
-```
-
----
-
-### 3.3 Person Object
-
-Describes a person related to the capability.
-
-#### 3.3.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **role** | `string` | **REQUIRED**. The role of the person in relation to the capability. E.g. owner, editor, viewer. |
-| **fullName** | `string` | **REQUIRED**. The full name of the person. |
-| **email** | `string` | The email address of the person. MUST match pattern `^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$`. |
-
-#### 3.3.2 Rules
-
-- Both `role` and `fullName` are mandatory.
-- No additional properties are allowed.
-
-#### 3.3.3 Person Object Example
-
-```yaml
-- role: owner
- fullName: "Jane Doe"
- email: "jane.doe@example.com"
-```
-
----
-
-### 3.4 Capability Object
-
-Defines the technical configuration of the capability.
-
-#### 3.4.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **exposes** | `Exposes[]` | **REQUIRED**. List of exposed server adapters. |
-| **consumes** | `Consumes[]` | **REQUIRED**. List of consumed client adapters. |
-
-#### 3.4.2 Rules
-
-- The `exposes` array MUST contain at least one entry.
-- The `consumes` array MUST contain at least one entry.
-- Each `consumes` entry MUST include both `baseUri` and `namespace` fields.
-- There are several types of exposed adapters and consumed sources objects, all will be described in following objects.
-- No additional properties are allowed.
-
-#### 3.4.3 Namespace Uniqueness Rule
-
-When multiple `consumes` entries are present:
-
-- Each `namespace` value MUST be unique across all consumes entries.
-- The `namespace` field is used for routing from the expose layer to the correct consumed source.
-- Duplicate namespace values will result in ambiguous routing and are forbidden.
-
-#### 3.4.4 Capability Object Example
-
-```yaml
-capability:
- exposes:
- - type: api
- port: 3000
- namespace: tasks-api
- resources:
- - path: /tasks
- description: "Endpoint to create tasks via the external API"
- operations:
- - method: POST
- label: Create Task
- call: api.create-task
- outputParameters:
- - type: string
- mapping: $.taskId
- consumes:
- - type: http
- namespace: api
- baseUri: https://api.example.com
- resources:
- - name: tasks
- label: Tasks API
- path: /tasks
- operations:
- - name: create-task
- label: Create Task
- method: POST
- inputParameters:
- - name: task_id
- in: path
- outputParameters:
- - name: taskId
- type: string
- value: $.data.id
-```
-
----
-
-### 3.5 Exposes Object
-
-Describes a server adapter that exposes functionality.
-
-> Update (schema v0.4): the exposition adapter is **API** with `type: "api"` (and a required `namespace`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore.
->
-
-#### 3.5.1 API Expose
-
-API exposition configuration.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"api"`. |
-| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. |
-| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. |
-| **authentication** | `Authentication` | Authentication configuration. |
-| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed API. |
-| **resources** | `ExposedResource[]` | **REQUIRED**. List of exposed resources. |
-
-#### 3.5.2 ExposedResource Object
-
-An exposed resource with **operations** and/or **forward** configuration.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **path** | `string` | **REQUIRED**. Path of the resource (supports `param` placeholders). |
-| **description** | `string` | **REQUIRED**. Used to provide *meaningful* information about the resource. In a world of agents, context is king. |
-| **name** | `string` | Technical name for the resource (used for references, pattern `^[a-zA-Z0-9-]+$`). |
-| **label** | `string` | Display name for the resource (likely used in UIs). |
-| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the resource. |
-| **operations** | `ExposedOperation[]` | Operations available on this resource. |
-| **forward** | `ForwardConfig` | Forwarding configuration to a consumed namespace. |
-
-#### 3.5.3 Rules
-
-- Both `description` and `path` are mandatory.
-- At least one of `operations` or `forward` MUST be present. Both can coexist on the same resource.
-- if both `operations` or `forward` are present, in case of conflict, `operations` takes precendence on `forward`.
-- No additional properties are allowed.
-
-#### 3.5.4 Address Validation Patterns
-
-- **Hostname**: `^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
-- **IPv4**: `^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
-- **IPv6**: `^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$`
-
-#### 3.5.5 Exposes Object Examples
-
-**API Expose with operations:**
-
-```yaml
-type: api
-port: 3000
-namespace: sample
-resources:
- - path: /status
- description: "Health check endpoint"
- name: health
- operations:
- - name: get-status
- method: GET
- call: api.health-check
- outputParameters:
- - type: string
- mapping: $.status
-```
-
-**API Expose with forward:**
-
-```yaml
-type: api
-port: 8080
-namespace: proxy
-resources:
- - path: /notion/{path}
- description: "Forward requests to the Notion API"
- forward:
- targetNamespace: notion
- trustedHeaders:
- - Notion-Version
-```
-
-**API Expose with both operations and forward:**
-
-```yaml
-type: api
-port: 9090
-namespace: hybrid
-resources:
- - path: /data/{path}
- description: "Resource with orchestrated operations and pass-through forwarding"
- operations:
- - name: get-summary
- method: GET
- call: api.get-summary
- forward:
- targetNamespace: api
- trustedHeaders:
- - Authorization
-```
-
----
-
-### 3.6 Consumes Object
-
-Describes a client adapter for consuming external APIs.
-
-> Update (schema v0.4): `targetUri` is now `baseUri`. The `headers` field has been removed — use `inputParameters` with `in: "header"` instead.
->
-
-#### 3.6.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Type of consumer. Valid values: `"http"`. |
-| **namespace** | `string` | Path suffix used for routing from exposes. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **baseUri** | `string` | **REQUIRED**. Base URI for the consumed API. Must be a valid http(s) URL (no `path` placeholder in the schema). |
-| **authentication** | Authentication Object | Authentication configuration. Defaults to `"inherit"`. |
-| **description** | `string` | **REQUIRED**. A description of the consumed API. The more meaningful it is, the easier for agent discovery. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters applied to all operations in this consumed API. |
-| **resources** | [ConsumedHttpResource Object] | **REQUIRED**. List of API resources. |
-
-#### 3.6.2 Rules
-
-- The `type` field MUST be `"http"`.
-- The `baseUri` field is required.
-- The `namespace` field is required and MUST be unique across all consumes entries.
-- The `namespace` value MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- The `description` field is required.
-- The `resources` array is required and MUST contain at least one entry.
-
-#### 3.6.3 Base URI Format
-
-The `baseUri` field MUST be a valid `http://` or `https://` URL, and may optionally include a base path.
-
-Example: `https://api.github.com` or `https://api.github.com/v3`
-
-#### 3.6.4 Consumes Object Example
-
-```yaml
-type: http
-namespace: github
-baseUri: https://api.github.com
-authentication:
- type: bearer
- token: "{{github_token}}"
-inputParameters:
- - name: Accept
- in: header
- value: "application/vnd.github.v3+json"
-resources:
- - name: users
- label: Users API
- path: /users/{username}
- operations:
- - name: get-user
- label: Get User
- method: GET
- inputParameters:
- - name: username
- in: path
- outputParameters:
- - name: userId
- type: string
- value: $.id
- - name: repos
- label: Repositories API
- path: /users/{username}/repos
- operations:
- - name: list-repos
- label: List Repositories
- method: GET
- inputParameters:
- - name: username
- in: path
- outputParameters:
- - name: repos
- type: array
- value: $
-```
-
----
-
-### 3.7 ConsumedHttpResource Object
-
-Describes an API resource that can be consumed from an external API.
-
-#### 3.7.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Technical name for the resource. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **label** | `string` | Display name of the resource. |
-| **description** | `string` | Description of the resource. |
-| **path** | `string` | **REQUIRED**. Path of the resource, relative to the consumes `baseUri`. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for this resource. |
-| **operations** | `ConsumedHttpOperation[]` | **REQUIRED**. List of operations for this resource. |
-
-#### 3.7.2 Rules
-
-- The `name` field MUST be unique within the parent consumes object's resources array.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- The `path` field will be appended to the parent consumes object's `baseUri`.
-- The `operations` array MUST contain at least one entry.
-- No additional properties are allowed.
-
-#### 3.7.3 ConsumedHttpResource Object Example
-
-```yaml
-name: users
-label: Users API
-path: /users/{username}
-inputParameters:
- - name: username
- in: path
-operations:
- - name: get-user
- label: Get User
- method: GET
- outputParameters:
- - name: userId
- type: string
- value: $.id
-```
-
----
-
-### 3.8 ConsumedHttpOperation Object
-
-Describes an operation that can be performed on a consumed resource.
-
-#### 3.8.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Technical name for the operation. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **label** | `string` | Display name of the operation. |
-| **description** | `string` | A longer description of the operation for documentation purposes. |
-| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `GET`. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for the operation. |
-| **outputRawFormat** | `string` | The raw format of the response. One of: `json`, `xml`, `avro`, `protobuf`, `csv`, `yaml` . Default: `json`. |
-| **outputParameters** | `ConsumedOutputParameter[]` | Output parameters extracted from the response via JsonPath. |
-| **body** | `RequestBody` | Request body configuration. |
-
-#### 3.8.2 Rules
-
-- The `name` field MUST be unique within the parent resource's operations array.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- Both `name` and `method` are mandatory.
-- No additional properties are allowed.
-
-#### 3.8.3 ConsumedHttpOperation Object Example
-
-```yaml
-name: get-user
-label: Get User Profile
-method: GET
-inputParameters:
- - name: username
- in: path
-outputParameters:
- - name: userId
- type: string
- value: $.id
- - name: username
- type: string
- value: $.login
- - name: email
- type: string
- value: $.email
-```
-
----
-
-### 3.9 ExposedOperation Object
-
-Describes an operation exposed on an exposed resource.
-
-> Update (schema v0.4): ExposedOperation now supports two modes via `oneOf` — **simple** (direct call with mapped output) and **orchestrated** (multi-step with named operation). The `call` and `with` fields are new. The `name` and `steps` fields are only required in orchestrated mode.
->
-
-#### 3.9.1 Fixed Fields
-
-All fields available on ExposedOperation:
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. |
-| **name** | `string` | Technical name for the operation (pattern `^[a-zA-Z0-9-]+$`). **REQUIRED in orchestrated mode only.** |
-| **label** | `string` | Display name for the operation (likely used in UIs). |
-| **description** | `string` | A longer description of the operation. Useful for agent discovery and documentation. |
-| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the operation. |
-| **call** | `string` | **Simple mode only**. Direct reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. |
-| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. |
-| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. |
-| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. |
-| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. |
-| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the operation's output parameters at the operation level. |
-
-#### 3.9.2 Modes (oneOf)
-
-**Simple mode** — A direct call to a single consumed operation:
-
-- `call` is **REQUIRED**
-- `with` is optional (inject parameters into the call)
-- `outputParameters` are `MappedOutputParameter[]` (type + mapping)
-- `steps` MUST NOT be present
-- `name` is optional
-
-**Orchestrated mode** — A multi-step orchestration:
-
-- `name` is **REQUIRED**
-- `steps` is **REQUIRED** (at least 1 entry)
-- `mappings` is optional — maps step outputs to the operation's output parameters at the operation level
-- `outputParameters` are `OrchestratedOutputParameter[]` (name + type)
-- `call` and `with` MUST NOT be present
-
-#### 3.9.3 Rules
-
-- Exactly one of the two modes MUST be used (simple or orchestrated).
-- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation.
-- In orchestrated mode, the `steps` array MUST contain at least one entry. Each step references a consumed operation using `{namespace}.{operationName}`.
-- The `method` field is always required regardless of mode.
-
-#### 3.9.4 ExposedOperation Object Examples
-
-**Simple mode (direct call):**
-
-```yaml
-method: GET
-label: Get User Profile
-call: github.get-user
-with:
- username: $this.sample.username
-outputParameters:
- - type: string
- mapping: $.login
- - type: number
- mapping: $.id
-```
-
-**Orchestrated mode (multi-step):**
-
-```yaml
-name: get-db
-method: GET
-label: Get Database
-inputParameters:
- - name: database_id
- in: path
- type: string
- description: The ID of the database to retrieve
-steps:
- - type: call
- name: fetch-db
- call: notion.get-database
- with:
- database_id: "$this.sample.database_id"
-mappings:
- - targetName: db_name
- value: "$.dbName"
-outputParameters:
- - name: db_name
- type: string
- - name: Api-Version
- type: string
-```
-
----
-
-### 3.10 RequestBody Object
-
-Describes request body configuration for consumed operations. `RequestBody` is a `oneOf` — exactly one of five subtypes must be used.
-
-#### 3.10.1 Subtypes
-
-**RequestBodyJson** — JSON body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"json"`. |
-| **data** | `string` | `object` | `array` | **REQUIRED**. The JSON payload. Can be a raw JSON string, an inline object, or an array. |
-
-**RequestBodyText** — Plain text, XML, or SPARQL body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. One of: `"text"`, `"xml"`, `"sparql"`. |
-| **data** | `string` | **REQUIRED**. The text payload. |
-
-**RequestBodyFormUrlEncoded** — URL-encoded form body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"formUrlEncoded"`. |
-| **data** | `string` | `object` | **REQUIRED**. Either a raw URL-encoded string or an object whose values are strings. |
-
-**RequestBodyMultipartForm** — Multipart form body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"multipartForm"`. |
-| **data** | `RequestBodyMultipartFormPart[]` | **REQUIRED**. Array of form parts. Each part has: `name` (required), `value` (required), `filename` (optional), `contentType` (optional). |
-
-**RequestBodyRaw** — Raw string body
-
-`RequestBody` can also be a plain `string`. When used with a YAML block scalar (`|`), the string is sent as-is. Interpreted as JSON by default.
-
-#### 3.10.2 Rules
-
-- Exactly one of the five subtypes must be used.
-- For structured subtypes, both `type` and `data` are mandatory.
-- No additional properties are allowed on any subtype.
-
-#### 3.10.3 RequestBody Examples
-
-**JSON body (object):**
-
-```yaml
-body:
- type: json
- data:
- hello: "world"
-```
-
-**JSON body (string):**
-
-```yaml
-body:
- type: json
- data: '{"key": "value"}'
-```
-
-**Text body:**
-
-```yaml
-body:
- type: text
- data: "Hello, world!"
-```
-
-**Form URL-encoded body:**
-
-```yaml
-body:
- type: formUrlEncoded
- data:
- username: "admin"
- password: "secret"
-```
-
-**Multipart form body:**
-
-```yaml
-body:
- type: multipartForm
- data:
- - name: "file"
- value: "base64content..."
- filename: "document.pdf"
- contentType: "application/pdf"
- - name: "description"
- value: "My uploaded file"
-```
-
-**Raw body:**
-
-```yaml
-body: |
- {
- "filter": {
- "property": "Status",
- "select": { "equals": "Active" }
- }
- }
-```
-
----
-
-### 3.11 InputParameter Objects
-
-> Update (schema v0.4): The single `InputParameter` object has been split into two distinct types: **ConsumedInputParameter** (used in consumes) and **ExposedInputParameter** (used in exposes, with additional `type` and `description` fields required).
->
-
-#### 3.11.1 ConsumedInputParameter Object
-
-Used in consumed resources and operations.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. |
-| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. |
-| **value** | `string` | Value or JSONPath reference. |
-
-**Rules:**
-
-- Both `name` and `in` are mandatory.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`.
-- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`.
-- A unique parameter is defined by the combination of `name` and `in`.
-- No additional properties are allowed.
-
-**ConsumedInputParameter Example:**
-
-```yaml
-- name: username
- in: path
-- name: page
- in: query
-- name: Authorization
- in: header
- value: "Bearer token"
-```
-
-#### 3.11.2 ExposedInputParameter Object
-
-Used in exposed resources and operations. Extends the consumed variant with `type` and `description` (both required) for agent discoverability, plus an optional `pattern` for validation.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. |
-| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. |
-| **type** | `string` | **REQUIRED**. Data type of the parameter. One of: `string`, `number`, `boolean`, `object`, `array`. |
-| **description** | `string` | **REQUIRED**. Human-readable description of the parameter. Essential for agent discovery. |
-| **pattern** | `string` | Optional regex pattern for parameter value validation. |
-| **value** | `string` | Default value or JSONPath reference. |
-
-**Rules:**
-
-- All of `name`, `in`, `type`, and `description` are mandatory.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`.
-- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`.
-- The `type` field MUST be one of: `"string"`, `"number"`, `"boolean"`, `"object"`, `"array"`.
-- No additional properties are allowed.
-
-**ExposedInputParameter Example:**
-
-```yaml
-- name: database_id
- in: path
- type: string
- description: The unique identifier of the Notion database
- pattern: "^[a-f0-9-]+$"
-- name: page_size
- in: query
- type: number
- description: Number of results per page (max 100)
-```
-
----
-
-### 3.12 OutputParameter Objects
-
-> Update (schema v0.4): The single `OutputParameter` object has been split into three distinct types: **ConsumedOutputParameter** (used in consumed operations), **MappedOutputParameter** (used in simple-mode exposed operations), and **OrchestratedOutputParameter** (used in orchestrated-mode exposed operations).
->
-
-#### 3.12.1 ConsumedOutputParameter Object
-
-Used in consumed operations to extract values from the raw API response.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Output parameter name. MUST match pattern `^[a-zA-Z0-9-_]+$`. |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-| **value** | `string` | **REQUIRED**. JsonPath expression to extract value from consumed function response. |
-
-**Rules:**
-
-- All three fields (`name`, `type`, `value`) are mandatory.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-_*]+$`.
-- The `value` field MUST start with `$`.
-- No additional properties are allowed.
-
-**ConsumedOutputParameter Example:**
-
-```yaml
-outputParameters:
- - name: dbName
- type: string
- value: $.title[0].text.content
- - name: dbId
- type: string
- value: $.id
-```
-
-#### 3.12.2 MappedOutputParameter Object
-
-Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-| **mapping** | `string` | `object` | **REQUIRED**. For scalar types (`string`, `number`, `boolean`): a JsonPath string. For `object`: an object with `properties` (recursive MappedOutputParameter map). For `array`: an object with `items` (recursive MappedOutputParameter). |
-
-**Subtypes by type:**
-
-- **`string`**, **`number`**, **`boolean`**: `mapping` is a JsonPath string (e.g. `$.login`)
-- **`object`**: `mapping` is `{ properties: { key: MappedOutputParameter, ... } }` — recursive
-- **`array`**: `mapping` is `{ items: MappedOutputParameter }` — recursive
-
-**Rules:**
-
-- Both `type` and `mapping` are mandatory.
-- No additional properties are allowed.
-
-**MappedOutputParameter Examples:**
-
-```yaml
-# Scalar mapping
-outputParameters:
- - type: string
- mapping: $.login
- - type: number
- mapping: $.id
-
-# Object mapping (recursive)
-outputParameters:
- - type: object
- mapping:
- properties:
- username:
- type: string
- mapping: $.login
- userId:
- type: number
- mapping: $.id
-
-# Array mapping (recursive)
-outputParameters:
- - type: array
- mapping:
- items:
- type: object
- mapping:
- properties:
- name:
- type: string
- mapping: $.name
-```
-
-#### 3.12.3 OrchestratedOutputParameter Object
-
-Used in **orchestrated mode** exposed operations. Declares an output by `name` and `type` (the value is populated via step mappings).
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Output parameter name. |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-
-**Subtypes by type:**
-
-- **`string`**, **`number`**, **`boolean`** (scalar): only `name` and `type`
-- **`array`**: adds `items` (recursive OrchestratedOutputParameter without `name`)
-- **`object`**: adds `properties` (map of name → recursive OrchestratedOutputParameter without `name`)
-
-**Rules:**
-
-- Both `name` and `type` are mandatory.
-- No additional properties are allowed.
-
-**OrchestratedOutputParameter Example:**
-
-```yaml
-outputParameters:
- - name: db_name
- type: string
- - name: Api-Version
- type: string
- - name: results
- type: array
- items:
- type: object
- properties:
- id:
- type: string
- title:
- type: string
-```
-
-#### 3.12.4 JsonPath roots (extensions)
-
-In a consumed resource, **`$`** refers to the *raw response payload* of the consumed operation (after decoding based on `outputRawFormat`). The root `$` gives direct access to the JSON response body.
-
-Example, if you consider the following JSON response :
-
-```json
-{
- "id": "154548",
- "titles": [
- {
- "text": {
- "content": "This is title[0].text.content",
- "author": "user1"
- }
- }
- ],
- "created_time": "2024-06-01T12:00:00Z"
-}
-```
-
-- `$.id` is `154548`
-- `$.titles[0].text.content` is `This is title[0].text.content`
-
-#### 3.12.5 Common patterns
-
-- `$.fieldName` — accesses a top-level field
-- `$.data.user.id` — accesses nested fields
-- `$.items[0]` — accesses array elements
-- `$.items[*].id` — accesses all ids in an array
-
----
-
-### 3.13 OperationStep Object
-
-Describes a single step in an orchestrated operation. `OperationStep` is a `oneOf` between two subtypes: **OperationStepCall** and **OperationStepLookup**, both sharing a common **OperationStepBase**.
-
-> Update (schema v0.4): OperationStep is now a discriminated union (`oneOf`) with a required `type` field (`"call"` or `"lookup"`) and a required `name` field. `OperationStepCall` uses `with` (WithInjector) instead of `inputParameters`. `OperationStepLookup` is entirely new.
->
-
-#### 3.13.1 OperationStepBase (shared fields)
-
-All operation steps share these base fields:
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Step type discriminator. One of: `"call"`, `"lookup"`. |
-| **name** | `string` | **REQUIRED**. Technical name for the step (pattern `^[a-zA-Z0-9-]+$`). Used as namespace for referencing step outputs in mappings and expressions. |
-
-#### 3.13.2 OperationStepCall
-
-Calls a consumed operation.
-
-**Fixed Fields** (in addition to base):
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"call"`. |
-| **name** | `string` | **REQUIRED**. Step name (from base). |
-| **call** | `string` | **REQUIRED**. Reference to consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. |
-| **with** | `WithInjector` | Parameter injection for the called operation. Keys are parameter names, values are strings or numbers (static values or `$this` references). |
-
-**Rules:**
-
-- `type`, `name`, and `call` are mandatory.
-- The `call` field MUST follow the format `{namespace}.{operationName}`.
-- The `namespace` portion MUST correspond to a namespace defined in one of the capability's consumes entries.
-- The `operationName` portion MUST correspond to an operation `name` defined in the consumes entry identified by the namespace.
-- `with` uses the same `WithInjector` object as simple-mode ExposedOperation (see §3.18).
-- No additional properties are allowed.
-
-#### 3.13.3 OperationStepLookup
-
-Performs a lookup against the output of a previous call step, matching values and extracting fields.
-
-**Fixed Fields** (in addition to base):
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"lookup"`. |
-| **name** | `string` | **REQUIRED**. Step name (from base). |
-| **index** | `string` | **REQUIRED**. Name of a previous call step whose output serves as the lookup table. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **match** | `string` | **REQUIRED**. Name of the key field in the index to match against. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **lookupValue** | `string` | **REQUIRED**. JsonPath expression resolving to the value(s) to look up. |
-| **outputParameters** | `string[]` | **REQUIRED**. List of field names to extract from the matched index entries (minimum 1 entry). |
-
-**Rules:**
-
-- `type`, `name`, `index`, `match`, `lookupValue`, and `outputParameters` are all mandatory.
-- `outputParameters` MUST contain at least one entry.
-- The `index` value MUST reference the `name` of a previous `call` step in the same orchestration.
-- No additional properties are allowed.
-
-#### 3.13.4 Call Reference Resolution
-
-The `call` value on an `OperationStepCall` is resolved as follows:
-
-1. Split the value on the `.` character into namespace and operationName
-2. Find the consumes entry with matching `namespace` field
-3. Within that consumes entry's resources, find the operation with matching `name` field
-4. Execute that operation as part of the orchestration sequence
-
-#### 3.13.5 OperationStep Object Examples
-
-**Call step with parameter injection:**
-
-```yaml
-steps:
- - type: call
- name: fetch-db
- call: notion.get-database
- with:
- database_id: $this.sample.database_id
-```
-
-**Lookup step (match against a previous call's output):**
-
-```yaml
-steps:
- - type: call
- name: list-users
- call: github.list-users
- - type: lookup
- name: find-user
- index: list-users
- match: email
- lookupValue: $this.sample.user_email
- outputParameters:
- - login
- - id
-```
-
-**Multi-step orchestration (call + lookup):**
-
-```yaml
-steps:
- - type: call
- name: get-entries
- call: api.list-entries
- - type: lookup
- name: resolve-entry
- index: get-entries
- match: entry_id
- lookupValue: $this.sample.target_id
- outputParameters:
- - title
- - status
- - type: call
- name: post-result
- call: slack.post-message
- with:
- text: $this.sample.title
-```
-
----
-
-### 3.14 StepOutputMapping Object
-
-Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation.
-
-#### 3.14.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **targetName** | `string` | **REQUIRED**. The name of the parameter to map to. It can be an input parameter of a next step or an output parameter of the exposed operation. |
-| **value** | `string` | **REQUIRED**. A JsonPath reference to the value to map from. E.g. `$.get-database.database_id`. |
-
-#### 3.14.2 Rules
-
-- Both `targetName` and `value` are mandatory.
-- No additional properties are allowed.
-
-#### 3.14.3 How mappings wire steps to exposed outputs
-
-A StepOutputMapping connects the **output parameters of a consumed operation** (called by the step) to the **output parameters of the exposed operation** (or to input parameters of subsequent steps).
-
-- **`targetName`** — refers to the `name` of an output parameter declared on the exposed operation, or the `name` of an input parameter of a subsequent step. The target parameter receives its value from the mapping.
-- **`value`** — a JsonPath expression where **`$`** is the root of the consumed operation's output parameters. The syntax `$.{outputParameterName}` references a named output parameter of the consumed operation called in this step.
-
-#### 3.14.4 End-to-end example
-
-Consider a consumed operation `notion.get-database` that declares:
-
-```yaml
-# In consumes → resources → operations
-name: "get-database"
-outputParameters:
- - name: "dbName"
- value: "$.title[0].text.content"
-```
-
-And the exposed side of the capability:
-
-```yaml
-# In exposes
-exposes:
- - type: "api"
- address: "localhost"
- port: 9090
- namespace: "sample"
- resources:
- - path: "/databases/{database_id}"
- name: "db"
- label: "Database resource"
- description: "Retrieve information about a Notion database"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "The unique identifier of the Notion database"
- operations:
- - name: "get-db"
- method: "GET"
- label: "Get Database"
- outputParameters:
- - name: "db_name"
- type: "string"
- steps:
- - type: "call"
- name: "fetch-db"
- call: "notion.get-database"
- with:
- database_id: "$this.sample.database_id"
- mappings:
- - targetName: "db_name"
- value: "$.dbName"
-```
-
-Here is what happens at orchestration time:
-
-1. The step `fetch-db` calls `notion.get-database`, which extracts `dbName` and `dbId` from the raw response via its own output parameters.
-2. The `with` injector passes `database_id` from the exposed input parameter (`$this.sample.database_id`) to the consumed operation.
-3. The mapping `targetName: "db_name"` refers to the exposed operation's output parameter `db_name`.
-4. The mapping `value: "$.dbName"` resolves to the value of the consumed operation's output parameter named `dbName`.
-5. As a result, the exposed output `db_name` is populated with the value extracted by `$.dbName` (i.e. `title[0].text.content` from the raw Notion API response).
-
-#### 3.14.5 StepOutputMapping Object Example
-
-```yaml
-mappings:
- - targetName: "db_name"
- value: "$.dbName"
-```
-
----
-
-### 3.15 `$this` Context Reference
-
-Describes how `$this` references work in `with` (WithInjector) and other expression contexts.
-
-> Update (schema v0.4): The former `OperationStepParameter` object (with `name` and `value` fields) has been replaced by `WithInjector` (see §3.18). This section now documents the `$this` expression root, which is used within `WithInjector` values.
->
-
-#### 3.15.1 The `$this` root
-
-In a `with` (WithInjector) value — whether on an ExposedOperation (simple mode) or an OperationStepCall — the **`$this`** root references the *current capability execution context*, i.e. values already resolved during orchestration.
-
-**`$this`** navigates the expose layer's input parameters using the path `$this.{exposeNamespace}.{inputParameterName}`. This allows a step or a simple-mode call to receive values that were provided by the caller of the exposed operation.
-
-- **`$this.{exposeNamespace}.{paramName}`** — accesses an input parameter of the exposed resource or operation identified by its namespace.
-- The `{exposeNamespace}` corresponds to the `namespace` of the exposed API.
-- The `{paramName}` corresponds to the `name` of an input parameter declared on the exposed resource or operation.
-
-#### 3.15.2 Example
-
-If the exposed API has namespace `sample` and an input parameter `database_id` declared on its resource, then:
-
-- `$this.sample.database_id` resolves to the value of `database_id` provided by the caller.
-
-**Usage in a WithInjector:**
-
-```yaml
-call: notion.get-database
-with:
- database_id: $this.sample.database_id
-```
-
----
-
-### 3.16 Authentication Object
-
-Defines authentication configuration. Four types are supported: basic, apikey, bearer, and digest.
-
-#### 3.16.1 Basic Authentication
-
-HTTP Basic Authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"basic"`. |
-| **username** | `string` | Username for basic auth. |
-| **password** | `string` | Password for basic auth. |
-
-**Example:**
-
-```yaml
-authentication:
- type: basic
- username: admin
- password: "secret_password"
-```
-
-#### 3.16.2 API Key Authentication
-
-API Key authentication via header or query parameter.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"apikey"`. |
-| **key** | `string` | API key name (header name or query parameter name). |
-| **value** | `string` | API key value. |
-| **placement** | `string` | Where to place the key. Valid values: `"header"`, `"query"`. |
-
-**Example:**
-
-```yaml
-authentication:
- type: apikey
- key: X-API-Key
- value: "{{api_key}}"
- placement: header
-```
-
-#### 3.16.3 Bearer Token Authentication
-
-Bearer token authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"bearer"`. |
-| **token** | `string` | Bearer token value. |
-
-**Example:**
-
-```yaml
-authentication:
- type: bearer
- token: "bearer_token"
-```
-
-#### 3.16.4 Digest Authentication
-
-HTTP Digest Authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"digest"`. |
-| **username** | `string` | Username for digest auth. |
-| **password** | `string` | Password for digest auth. |
-
-**Example:**
-
-```yaml
-authentication:
- type: digest
- username: admin
- password: "secret_password"
-```
-
-#### 3.16.5 Rules
-
-- Only one authentication type can be used per authentication object.
-- The `type` field determines which additional fields are required or allowed.
-- Authentication can be specified at multiple levels (exposes, consumes) with inner levels overriding outer levels.
-
----
-
-### 3.17 ForwardConfig Object
-
-Defines forwarding configuration for an exposed resource to pass requests through to a consumed namespace.
-
-> Update (schema v0.4): Renamed from `ForwardHeaders` to `ForwardConfig`. The `targetNamespaces` array has been replaced by a single `targetNamespace` string.
->
-
-#### 3.17.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **targetNamespace** | `string` | **REQUIRED**. The consumer namespace to forward requests to. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **trustedHeaders** | [`string`] | **REQUIRED**. List of headers allowed to be forwarded (minimum 1 entry). No wildcards supported. |
-
-#### 3.17.2 Rules
-
-- The `targetNamespace` field is mandatory and MUST reference a valid namespace from one of the capability's consumes entries.
-- The `trustedHeaders` array is mandatory and MUST contain at least one entry.
-- Header names in `trustedHeaders` are case-insensitive (following HTTP header conventions).
-- Only headers listed in `trustedHeaders` will be forwarded to the consumed source.
-- No additional properties are allowed.
-
-#### 3.17.3 ForwardConfig Object Example
-
-```yaml
-forward:
- targetNamespace: notion
- trustedHeaders:
- - Authorization
- - Notion-Version
-```
-
----
-
-### 3.18 WithInjector Object
-
-Defines parameter injection for simple-mode exposed operations. Used with the `with` field on an ExposedOperation to inject values into the called consumed operation.
-
-> New in schema v0.4.
->
-
-#### 3.18.1 Shape
-
-`WithInjector` is an object whose keys are parameter names and whose values are static values or `$this` references.
-
-- Each key corresponds to a parameter `name` in the consumed operation's `inputParameters`.
-- Each value is a `string` or a `number`: either a static value or a `$this.{namespace}.{paramName}` reference.
-
-#### 3.18.2 Rules
-
-- The keys MUST correspond to valid parameter names in the consumed operation being called.
-- Values can be strings or numbers.
-- String values can use the `$this` root to reference exposed input parameters (same as in OperationStepParameter).
-- No additional constraints.
-
-#### 3.18.3 WithInjector Object Example
-
-```yaml
-call: github.get-user
-with:
- username: $this.sample.username
- Accept: "application/json"
- maxRetries: 3
-```
-
----
-
-### 3.19 ExternalRef Object
-
-> **Updated**: ExternalRef is now a discriminated union (`oneOf`) with two variants — **file-resolved** (for local development) and **runtime-resolved** (for production). Variables are explicitly declared via a `keys` map.
->
-
-Declares an external reference that provides variables to the capability. External references are declared at the root level of the Naftiko document via the `externalRefs` array.
-
-`ExternalRef` is a `oneOf` — exactly one of the two variants must be used.
-
-#### 3.19.1 File-Resolved ExternalRef
-
-Loads variables from a local file. Intended for **local development only**.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **description** | `string` | **REQUIRED**. Used to provide *meaningful* information about the external reference. In a world of agents, context is king. |
-| **type** | `string` | **REQUIRED**. MUST be `"environment"`. |
-| **resolution** | `string` | **REQUIRED**. MUST be `"file"`. |
-| **uri** | `string` | **REQUIRED**. URI pointing to the file (e.g. `file:///path/to/env.json`). |
-| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the resolved file content. |
-
-**Rules:**
-
-- All fields (`name`, `description`, `type`, `resolution`, `uri`, `keys`) are mandatory.
-- No additional properties are allowed.
-
-#### 3.19.2 Runtime-Resolved ExternalRef
-
-Variables are injected by the execution environment at startup (default). The capability document does **not** specify where the values come from — this is delegated to the deployment platform.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **type** | `string` | **REQUIRED**. MUST be `"environment"`. |
-| **resolution** | `string` | **REQUIRED.** MUST be `"runtime"`. |
-| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the runtime context. |
-
-**Rules:**
-
-- `name`, `type`, and `keys` are mandatory.
-- `resolution` is optional; when present MUST be `"runtime"`.
-- No additional properties are allowed.
-
-Typical production providers include:
-
-- **HashiCorp Vault** — centralized secrets management
-- **Kubernetes Secrets** / **ConfigMaps** — native K8s secret injection
-- **AWS Secrets Manager** / **AWS SSM Parameter Store**
-- **Azure Key Vault**
-- **GCP Secret Manager**
-- **Docker Secrets** — for containerized deployments
-- **CI/CD pipeline variables** (GitHub Actions secrets, GitLab CI variables, etc.)
-
-#### 3.19.3 ExternalRefKeys Object
-
-A map of key-value pairs that define the variables to be injected from the external reference.
-
-- Each **key** is the variable name used for injection (available as `\{\{key\}\}` in the capability definition)
-- Each **value** is the corresponding key in the resolved file content or runtime context
-
-Example: `{"notion_token": "NOTION_INTEGRATION_TOKEN"}` means the value of `NOTION_INTEGRATION_TOKEN` in the source will be injected as `{{notion_token}}` in the capability definition.
-
-**Schema:**
-
-```json
-{
- "type": "object",
- "additionalProperties": { "type": "string" }
-}
-```
-
-#### 3.19.4 Rules
-
-- Each `name` value MUST be unique across all `externalRefs` entries.
-- The `name` value MUST NOT collide with any `consumes` namespace to avoid ambiguity.
-- The `keys` map MUST contain at least one entry.
-- No additional properties are allowed on either variant.
-
-
-
-#### 3.19.5 ExternalRef Object Examples
-
-**File resolution (development):**
-
-```yaml
-externalRefs:
- - name: "notion-env"
- type: "environment"
- description: "External reference to Notion API for accessing project data stored in Notion."
- resolution: file
- uri: "file:///path/to/notion_env.json"
- keys:
- notion_token: "NOTION_INTEGRATION_TOKEN"
- notion_projects_db_id: "PROJECTS_DATABASE_ID"
- notion_time_tracker_db_id: "TIME_TRACKER_DATABASE_ID"
-```
-
-**Runtime resolution (production):**
-
-```yaml
-externalRefs:
- - name: "secrets"
- type: "environment"
- resolution: runtime
- keys:
- notion_token: "NOTION_INTEGRATION_TOKEN"
- github_token: "GITHUB_TOKEN"
-```
-
-**Minimal runtime (resolution omitted — defaults to runtime):**
-
-```yaml
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- api_key: "API_KEY"
-```
-
----
-
-### 3.20 Expression Syntax
-
-Variables declared in `externalRefs` via the `keys` map are injected into the capability document using mustache-style `\{\{variable\}\}` expressions.
-
-#### 3.20.1 Format
-
-The expression format is `\{\{key\}\}`, where `key` is a variable name declared in the `keys` map of an `externalRefs` entry.
-
-Expressions can appear in any `string` value within the document, including authentication tokens, header values, and input parameter values.
-
-#### 3.20.2 Resolution
-
-At runtime, expressions are resolved as follows:
-
-1. Find the `externalRefs` entry whose `keys` map contains the referenced variable name
-2. Look up the corresponding source key in the `keys` map
-3. Resolve the source key value using the strategy defined by `resolution` (`file` lookup or `runtime` injection)
-4. Replace the `\{\{variable\}\}` expression with the resolved value
-
-If a referenced variable is not declared in any `externalRefs` entry's `keys`, the document MUST be considered invalid.
-
-#### 3.20.3 Relationship with `$this`
-
-`\{\{variable\}\}` expressions and `$this` references serve different purposes:
-
-- `\{\{variable\}\}` resolves **static configuration** from external references (secrets, environment variables) declared via `keys`
-- `$this.{exposeNamespace}.{paramName}` resolves **runtime orchestration** values from the expose layer's input parameters
-
-The two expression systems are independent and MUST NOT be mixed.
-
-#### 3.20.4 Expression Examples
-
-```yaml
-# Authentication token from external ref
-authentication:
- type: bearer
- token: "{{notion_token}}"
-
-# Input parameter with header value from external ref
-inputParameters:
- - name: Authorization
- in: header
- value: "Bearer {{api_key}}"
-
-# Corresponding externalRefs declaration
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
- api_key: "API_KEY"
-```
-
----
-
-## 4. Complete Examples
-
-This section provides progressive examples — from the simplest capability to a full-featured one — to illustrate the main patterns of the specification. All examples are pseudo-functional and use realistic API shapes.
-
-### 4.1 Forward-only capability (proxy)
-
-The simplest capability: forward incoming requests to a consumed API without any transformation.
-
-```yaml
----
-naftiko: "0.4"
-info:
- label: "Notion Proxy"
- description: "Pass-through proxy to the Notion API for development and debugging"
- tags:
- - proxy
- - notion
- created: "2026-02-01"
- modified: "2026-02-01"
-
-capability:
- exposes:
- - type: "api"
- port: 8080
- namespace: "proxy"
- resources:
- - path: "/notion/{path}"
- description: "Forwards all requests to the Notion API"
- forward:
- targetNamespace: "notion"
- trustedHeaders:
- - "Authorization"
- - "Notion-Version"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API"
- baseUri: "https://api.notion.com/v1"
- resources:
- - name: "all"
- path: "/{path}"
- operations:
- - name: "any"
- method: "GET"
-```
-
-### 4.2 Simple-mode capability (direct call)
-
-A single exposed operation that directly calls a consumed operation, maps parameters with `with`, and extracts output.
-
-```yaml
----
-naftiko: "0.4"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- github_token: "GITHUB_TOKEN"
-info:
- label: "GitHub User Lookup"
- description: "Exposes a simplified endpoint to retrieve GitHub user profiles"
- tags:
- - github
- - users
- created: "2026-02-01"
- modified: "2026-02-01"
-
-capability:
- exposes:
- - type: "api"
- port: 3000
- namespace: "app"
- resources:
- - path: "/users/{username}"
- description: "Look up a GitHub user by username"
- name: "user"
- inputParameters:
- - name: "username"
- in: "path"
- type: "string"
- description: "The GitHub username to look up"
- operations:
- - method: "GET"
- label: "Get User"
- call: "github.get-user"
- with:
- username: "$this.app.username"
- outputParameters:
- - type: "string"
- mapping: "$.login"
- - type: "string"
- mapping: "$.email"
- - type: "number"
- mapping: "$.id"
-
- consumes:
- - type: "http"
- namespace: "github"
- description: "GitHub REST API v3"
- baseUri: "https://api.github.com"
- authentication:
- type: "bearer"
- token: "{{github_token}}"
- resources:
- - name: "users"
- path: "/users/{username}"
- label: "Users"
- operations:
- - name: "get-user"
- label: "Get User"
- method: "GET"
- inputParameters:
- - name: "username"
- in: "path"
- outputParameters:
- - name: "login"
- type: "string"
- value: "$.login"
- - name: "email"
- type: "string"
- value: "$.email"
- - name: "id"
- type: "number"
- value: "$.id"
-```
-
-### 4.3 Orchestrated capability (multi-step call)
-
-An exposed operation that chains two consumed operations using named steps and `with`.
-
-```yaml
----
-naftiko: "0.4"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
-info:
- label: "Database Inspector"
- description: "Retrieves a Notion database then queries its contents in a single exposed operation"
- tags:
- - notion
- - orchestration
- created: "2026-02-10"
- modified: "2026-02-10"
-
-capability:
- exposes:
- - type: "api"
- port: 9090
- namespace: "inspector"
- resources:
- - path: "/databases/{database_id}/summary"
- description: "Returns database metadata and first page of results"
- name: "db-summary"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "The Notion database ID"
- operations:
- - name: "get-summary"
- method: "GET"
- label: "Get Database Summary"
- steps:
- - type: "call"
- name: "fetch-db"
- call: "notion.get-database"
- with:
- database_id: "$this.inspector.database_id"
- - type: "call"
- name: "query-db"
- call: "notion.query-database"
- with:
- database_id: "$this.inspector.database_id"
- mappings:
- - targetName: "db_name"
- value: "$.fetch-db.dbName"
- - targetName: "row_count"
- value: "$.query-db.resultCount"
- outputParameters:
- - name: "db_name"
- type: "string"
- - name: "row_count"
- type: "number"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API"
- baseUri: "https://api.notion.com/v1"
- authentication:
- type: "bearer"
- token: "{{notion_token}}"
- inputParameters:
- - name: "Notion-Version"
- in: "header"
- value: "2022-06-28"
- resources:
- - name: "databases"
- path: "/databases/{database_id}"
- label: "Databases"
- operations:
- - name: "get-database"
- label: "Get Database"
- method: "GET"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "dbName"
- type: "string"
- value: "$.title[0].text.content"
- - name: "dbId"
- type: "string"
- value: "$.id"
- - name: "queries"
- path: "/databases/{database_id}/query"
- label: "Database queries"
- operations:
- - name: "query-database"
- label: "Query Database"
- method: "POST"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "resultCount"
- type: "number"
- value: "$.results.length()"
- - name: "results"
- type: "array"
- value: "$.results"
-```
-
-### 4.4 Orchestrated capability with lookup step
-
-Demonstrates a `lookup` step that cross-references the output of a previous call to enrich data.
-
-```yaml
----
-naftiko: "0.4"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- hr_api_key: "HR_API_KEY"
-info:
- label: "Team Member Resolver"
- description: "Resolves team member details by matching email addresses from a project tracker"
- tags:
- - hr
- - lookup
- created: "2026-02-15"
- modified: "2026-02-15"
-
-capability:
- exposes:
- - type: "api"
- port: 4000
- namespace: "team"
- resources:
- - path: "/resolve/{email}"
- description: "Finds a team member by email and returns their profile"
- name: "resolve"
- inputParameters:
- - name: "email"
- in: "path"
- type: "string"
- description: "Email address to look up"
- operations:
- - name: "resolve-member"
- method: "GET"
- label: "Resolve Team Member"
- steps:
- - type: "call"
- name: "list-members"
- call: "hr.list-employees"
- - type: "lookup"
- name: "find-member"
- index: "list-members"
- match: "email"
- lookupValue: "$this.team.email"
- outputParameters:
- - "fullName"
- - "department"
- - "role"
- mappings:
- - targetName: "name"
- value: "$.find-member.fullName"
- - targetName: "department"
- value: "$.find-member.department"
- - targetName: "role"
- value: "$.find-member.role"
- outputParameters:
- - name: "name"
- type: "string"
- - name: "department"
- type: "string"
- - name: "role"
- type: "string"
-
- consumes:
- - type: "http"
- namespace: "hr"
- description: "Internal HR system API"
- baseUri: "https://hr.internal.example.com/api"
- authentication:
- type: "apikey"
- key: "X-Api-Key"
- value: "{{hr_api_key}}"
- placement: "header"
- resources:
- - name: "employees"
- path: "/employees"
- label: "Employees"
- operations:
- - name: "list-employees"
- label: "List All Employees"
- method: "GET"
- outputParameters:
- - name: "email"
- type: "string"
- value: "$.items[*].email"
- - name: "fullName"
- type: "string"
- value: "$.items[*].name"
- - name: "department"
- type: "string"
- value: "$.items[*].department"
- - name: "role"
- type: "string"
- value: "$.items[*].role"
-```
-
-### 4.5 Full-featured capability (mixed modes)
-
-Combines forward proxy, simple-mode operations, orchestrated multi-step with lookup, and multiple consumed sources.
-
-```yaml
----
-naftiko: "0.4"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
- github_token: "GITHUB_TOKEN"
-info:
- label: "Project Dashboard"
- description: "Aggregates project data from Notion and GitHub into a unified API, with a pass-through proxy for direct access"
- tags:
- - dashboard
- - notion
- - github
- created: "2026-02-20"
- modified: "2026-02-20"
- stakeholders:
- - role: "owner"
- fullName: "Jane Doe"
- email: "jane.doe@example.com"
- - role: "editor"
- fullName: "John Smith"
- email: "john.smith@example.com"
-
-capability:
- exposes:
- - type: "api"
- port: 9090
- namespace: "dashboard"
- resources:
- # --- Forward proxy (simplest) ---
- - path: "/github/{path}"
- description: "Direct pass-through to the GitHub API for debugging"
- forward:
- targetNamespace: "github"
- trustedHeaders:
- - "Authorization"
-
- # --- Simple mode (direct call) ---
- - path: "/repos/{owner}/{repo}"
- description: "Retrieve a GitHub repository summary"
- name: "repo"
- inputParameters:
- - name: "owner"
- in: "path"
- type: "string"
- description: "Repository owner (user or organization)"
- - name: "repo"
- in: "path"
- type: "string"
- description: "Repository name"
- operations:
- - method: "GET"
- label: "Get Repository"
- call: "github.get-repo"
- with:
- owner: "$this.dashboard.owner"
- repo: "$this.dashboard.repo"
- outputParameters:
- - type: "string"
- mapping: "$.full_name"
- - type: "number"
- mapping: "$.stargazers_count"
- - type: "string"
- mapping: "$.language"
-
- # --- Orchestrated mode (multi-step call + lookup) ---
- - path: "/projects/{database_id}/contributors"
- description: "Lists project tasks from Notion and enriches each assignee with GitHub profile data"
- name: "contributors"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "Notion database ID for the project tracker"
- operations:
- - name: "list-contributors"
- method: "GET"
- label: "List Project Contributors"
- steps:
- - type: "call"
- name: "query-tasks"
- call: "notion.query-database"
- with:
- database_id: "$this.dashboard.database_id"
- - type: "call"
- name: "list-github-users"
- call: "github.list-org-members"
- with:
- org: "naftiko"
- - type: "lookup"
- name: "match-contributors"
- index: "list-github-users"
- match: "login"
- lookupValue: "$.query-tasks.assignee"
- outputParameters:
- - "login"
- - "avatar_url"
- - "html_url"
- mappings:
- - targetName: "contributors"
- value: "$.match-contributors"
- outputParameters:
- - name: "contributors"
- type: "array"
- items:
- type: "object"
- properties:
- login:
- type: "string"
- avatar_url:
- type: "string"
- html_url:
- type: "string"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API for database and page operations"
- baseUri: "https://api.notion.com/v1"
- authentication:
- type: "bearer"
- token: "{{notion_token}}"
- inputParameters:
- - name: "Notion-Version"
- in: "header"
- value: "2022-06-28"
- resources:
- - name: "db-query"
- path: "/databases/{database_id}/query"
- label: "Database Query"
- operations:
- - name: "query-database"
- label: "Query Database"
- method: "POST"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "assignee"
- type: "string"
- value: "$.results[*].properties.Assignee.people[0].name"
- - name: "taskName"
- type: "string"
- value: "$.results[*].properties.Name.title[0].text.content"
-
- - type: "http"
- namespace: "github"
- description: "GitHub REST API for repository and user operations"
- baseUri: "https://api.github.com"
- authentication:
- type: "bearer"
- token: "{{github_token}}"
- resources:
- - name: "repos"
- path: "/repos/{owner}/{repo}"
- label: "Repositories"
- operations:
- - name: "get-repo"
- label: "Get Repository"
- method: "GET"
- inputParameters:
- - name: "owner"
- in: "path"
- - name: "repo"
- in: "path"
- outputParameters:
- - name: "full_name"
- type: "string"
- value: "$.full_name"
- - name: "stargazers_count"
- type: "number"
- value: "$.stargazers_count"
- - name: "language"
- type: "string"
- value: "$.language"
- - name: "org-members"
- path: "/orgs/{org}/members"
- label: "Organization Members"
- operations:
- - name: "list-org-members"
- label: "List Organization Members"
- method: "GET"
- inputParameters:
- - name: "org"
- in: "path"
- outputParameters:
- - name: "login"
- type: "string"
- value: "$[*].login"
- - name: "avatar_url"
- type: "string"
- value: "$[*].avatar_url"
- - name: "html_url"
- type: "string"
- value: "$[*].html_url"
-```
-
----
-
-## 5. Versioning
-
-The Naftiko Specification uses semantic versioning. The `naftiko` field in the Naftiko Object specifies the exact version of the specification (e.g., `"0.4"`).
-
-Tools processing Naftiko documents MUST validate this field to ensure compatibility with the specification version they support.
-
----
-
-This specification defines how to describe modular, composable capabilities that consume multiple sources and expose unified interfaces, supporting orchestration, authentication, and flexible routing patterns.
\ No newline at end of file
diff --git a/src/main/resources/specs/naftiko-specification-v0.5.md b/src/main/resources/specs/naftiko-specification-v0.5.md
deleted file mode 100644
index 1362a4e..0000000
--- a/src/main/resources/specs/naftiko-specification-v0.5.md
+++ /dev/null
@@ -1,2321 +0,0 @@
-# Naftiko Specification
-
-Version: 0.5
-Created by: Thomas Eskenazi
-Category: Sepcification
-Last updated time: March 5, 2026 12:40 PM
-Reviewers: Kin Lane, Jerome Louvel, Jérémie Tarnaud, Antoine Buhl
-Status: Draft
-
-# Naftiko Specification v0.5
-
-**Version 0.5**
-
-**Publication Date:** March 2026
-
----
-
-- **Table of Contents**
-
-## 1. Introduction
-
-The Naftiko Specification defines a standard, language-agnostic interface for describing modular, composable capabilities. In short, a **capability** is a functional unit that consumes external APIs (sources) and exposes adapters that allow other systems to interact with it.
-
-A Naftiko capability focuses on declaring the **integration intent** — what a system needs to consume and what it exposes — rather than implementation details. This higher-level abstraction makes capabilities naturally suitable for AI-driven discovery, orchestration and integration use cases, and beyond. When properly defined, a capability can be discovered, orchestrated, validated and executed with minimal implementation logic. The specification enables description of:
-
-- **Consumed sources**: External APIs or services that the capability uses
-- **Exposed adapters**: Server interfaces that the capability provides (HTTP, REST, etc.)
-- **Orchestration**: How calls to consumed sources are combined and mapped to realize exposed functions
-- **External references**: Variables and resources resolved from external sources
-
-### 1.1 Schema Access
-
-The JSON Schema for the Naftiko Specification is available in two forms:
-
-- **Raw file** — The schema source file is hosted on GitHub: [capability-schema.json](https://github.com/naftiko/framework/blob/main/src/main/resources/schemas/capability-schema.json)
-- **Interactive viewer** — A human-friendly viewer is available at: [Schema Viewer](https://naftiko.github.io/schema-viewer/)
-
-### 1.2 Core Objects
-
-**Capability**: The central object that defines a modular functional unit with clear input/output contracts.
-
-**Consumes**: External sources (APIs, services) that the capability uses to realize its operations.
-
-**Exposes**: Server adapters that provide access to the capability's operations.
-
-**Resources**: API endpoints that group related operations.
-
-**Operations**: Individual HTTP operations (GET, POST, etc.) that can be performed on resources.
-
-**Namespace**: A unique identifier for consumed sources, used for routing and mapping with the expose layer.
-
-**MCP Server**: An exposition adapter that exposes capability operations as MCP tools, enabling AI agent integration via Streamable HTTP or stdio transport.
-
-**ExternalRef**: A declaration of an external reference providing variables to the capability. Two variants: file-resolved (for development) and runtime-resolved (for production). Variables are explicitly declared via a `keys` map.
-
-### 1.3 Related Specifications.
-
-
-
-Three specifications that work better together.
-
-| | **OpenAPI** | **Arazzo** | **OpenCollections** | **Naftiko** |
-| --- | --- | --- | --- | --- |
-| **Focus** | Defines *what* your API is — the contract, the schema, the structure. | Defines *how* API calls are sequenced — the workflows between endpoints. | Defines *how* to use your API — the scenarios, the runnable collections. | Defines *what* a capability consumes and exposes — the integration intent. |
-| **Scope** | Single API surface | Workflows across one or more APIs | Runnable collections of API calls | Modular capability spanning multiple APIs |
-| **Key strengths** | ✓ Endpoints & HTTP methods, ✓ Request/response schemas, ✓ Authentication requirements, ✓ Data types & validation, ✓ SDK & docs generation | ✓ Multi-step sequences, ✓ Step dependencies & data flow, ✓ Success/failure criteria, ✓ Reusable workflow definitions | ✓ Runnable, shareable collections, ✓ Pre-request scripts & tests, ✓ Environment variables, ✓ Living, executable docs | ✓ Consume/expose duality, ✓ Namespace-based routing, ✓ Orchestration & forwarding, ✓ AI-driven discovery, ✓ Composable capabilities |
-| **Analogy** | The *parts list* and dimensions | The *assembly sequence* between parts | The *step-by-step assembly guide* you can run | The *product blueprint* — what goes in, what comes out |
-| **Best used when you need to…** | Define & document an API contract, generate SDKs, validate payloads | Describe multi-step API workflows with dependencies | Share runnable API examples, test workflows, onboard developers | Declare a composable capability that consumes sources and exposes unified interfaces |
-
-**OpenAPI** tells you the shape of the door. **Arazzo** describes the sequence of doors to walk through. **OpenCollections** lets you actually walk through them. **Naftiko** combines the features of those 3 specs into a single, coherent spec, reducing complexity and offering consistent tooling out of the box
-
----
-
-## 2. Format
-
-Naftiko specifications can be represented in YAML format, complying with the provided Naftiko schema which is made available in both JSON Schema and [JSON Structure](https://json-structure.org/) formats.
-
-All field names in the specification are **case-sensitive**.
-
-Naftiko Objects expose two types of fields:
-
-- **Fixed fields**: which have a declared name
-- **Patterned fields**: which have a declared pattern for the field name
-
----
-
-## 3. Objects and Fields
-
-### 3.1 Naftiko Object
-
-This is the root object of the Naftiko document.
-
-#### 3.1.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **naftiko** | `string` | **REQUIRED**. Version of the Naftiko schema. MUST be `"0.5"` for this version. |
-| **info** | `Info` | *Recommended*. Metadata about the capability. |
-| **capability** | `Capability` | **REQUIRED**. Technical configuration of the capability including sources and adapters. |
-| **externalRefs** | `ExternalRef[]` | List of external references for variable injection. Each entry declares injected variables via a `keys` map. |
-
-#### 3.1.2 Rules
-
-- The `naftiko` field MUST be present and MUST have the value `"0.5"` for documents conforming to this version of the specification.
-- The `capability` object MUST be present. The `info` object is recommended.
-- The `externalRefs` field is OPTIONAL. When present, it MUST contain at least one entry.
-- No additional properties are allowed at the root level.
-
----
-
-### 3.2 Info Object
-
-Provides metadata about the capability.
-
-#### 3.2.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **label** | `string` | **REQUIRED**. The display name of the capability. |
-| **description** | `string` | *Recommended*. A description of the capability. The more meaningful it is, the easier for agent discovery. |
-| **tags** | `string[]` | List of tags to help categorize the capability for discovery and filtering. |
-| **created** | `string` | Date the capability was created (format: `YYYY-MM-DD`). |
-| **modified** | `string` | Date the capability was last modified (format: `YYYY-MM-DD`). |
-| **stakeholders** | `Person[]` | List of stakeholders related to this capability (for discovery and filtering). |
-
-#### 3.2.2 Rules
-
-- The `label` field is mandatory. The `description` field is recommended to improve agent discovery.
-- No additional properties are allowed.
-
-#### 3.2.3 Info Object Example
-
-```yaml
-info:
- label: Notion Page Creator
- description: Creates and manages Notion pages with rich content formatting
- tags:
- - notion
- - automation
- created: "2026-02-17"
- modified: "2026-02-17"
- stakeholders:
- - role: owner
- fullName: "Jane Doe"
- email: "jane.doe@example.
-```
-
----
-
-### 3.3 Person Object
-
-Describes a person related to the capability.
-
-#### 3.3.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **role** | `string` | **REQUIRED**. The role of the person in relation to the capability. E.g. owner, editor, viewer. |
-| **fullName** | `string` | **REQUIRED**. The full name of the person. |
-| **email** | `string` | The email address of the person. MUST match pattern `^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$`. |
-
-#### 3.3.2 Rules
-
-- Both `role` and `fullName` are mandatory.
-- No additional properties are allowed.
-
-#### 3.3.3 Person Object Example
-
-```yaml
-- role: owner
- fullName: "Jane Doe"
- email: "jane.doe@example.com"
-```
-
----
-
-### 3.4 Capability Object
-
-Defines the technical configuration of the capability.
-
-#### 3.4.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **exposes** | `Exposes[]` | List of exposed server adapters. Each entry is an API Expose (`type: "api"`) or an MCP Expose (`type: "mcp"`). |
-| **consumes** | `Consumes[]` | List of consumed client adapters. |
-
-#### 3.4.2 Rules
-
-- At least one of `exposes` or `consumes` MUST be present.
-- When present, the `exposes` array MUST contain at least one entry.
-- When present, the `consumes` array MUST contain at least one entry.
-- Each `consumes` entry MUST include both `baseUri` and `namespace` fields.
-- There are several types of exposed adapters and consumed sources objects, all will be described in following objects.
-- No additional properties are allowed.
-
-#### 3.4.3 Namespace Uniqueness Rule
-
-When multiple `consumes` entries are present:
-
-- Each `namespace` value MUST be unique across all consumes entries.
-- The `namespace` field is used for routing from the expose layer to the correct consumed source.
-- Duplicate namespace values will result in ambiguous routing and are forbidden.
-
-#### 3.4.4 Capability Object Example
-
-```yaml
-capability:
- exposes:
- - type: api
- port: 3000
- namespace: tasks-api
- resources:
- - path: /tasks
- description: "Endpoint to create tasks via the external API"
- operations:
- - method: POST
- label: Create Task
- call: api.create-task
- outputParameters:
- - type: string
- mapping: $.taskId
- consumes:
- - type: http
- namespace: api
- baseUri: https://api.example.com
- resources:
- - name: tasks
- label: Tasks API
- path: /tasks
- operations:
- - name: create-task
- label: Create Task
- method: POST
- inputParameters:
- - name: task_id
- in: path
- outputParameters:
- - name: taskId
- type: string
- value: $.data.id
-```
-
----
-
-### 3.5 Exposes Object
-
-Describes a server adapter that exposes functionality.
-
-> Update (schema v0.5): Two exposition adapter types are now supported — **API** (`type: "api"`) and **MCP** (`type: "mcp"`). Legacy `httpProxy` / `rest` exposition types are not part of the JSON Schema anymore.
->
-
-#### 3.5.1 API Expose
-
-API exposition configuration.
-
-> Update (schema v0.5): The Exposes object is now a discriminated union (`oneOf`) between **API** (`type: "api"`, this section) and **MCP** (`type: "mcp"`, see §3.5.4). The `type` field acts as discriminator.
->
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"api"`. |
-| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. |
-| **port** | `integer` | **REQUIRED**. Port number. MUST be between 1 and 65535. |
-| **authentication** | `Authentication` | Authentication configuration. |
-| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed API. |
-| **resources** | `ExposedResource[]` | **REQUIRED**. List of exposed resources. |
-
-#### 3.5.2 ExposedResource Object
-
-An exposed resource with **operations** and/or **forward** configuration.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **path** | `string` | **REQUIRED**. Path of the resource (supports `param` placeholders). |
-| **description** | `string` | *Recommended*. Used to provide *meaningful* information about the resource. In a world of agents, context is king. |
-| **name** | `string` | Technical name for the resource (used for references, pattern `^[a-zA-Z0-9-]+$`). |
-| **label** | `string` | Display name for the resource (likely used in UIs). |
-| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the resource. |
-| **operations** | `ExposedOperation[]` | Operations available on this resource. |
-| **forward** | `ForwardConfig` | Forwarding configuration to a consumed namespace. |
-
-#### 3.5.3 Rules
-
-- The `path` field is mandatory. The `description` field is recommended to provide meaningful context for agent discovery.
-- At least one of `operations` or `forward` MUST be present. Both can coexist on the same resource.
-- if both `operations` or `forward` are present, in case of conflict, `operations` takes precendence on `forward`.
-- No additional properties are allowed.
-
-#### 3.5.4 MCP Expose
-
-MCP Server exposition configuration. Exposes capability operations as MCP tools over Streamable HTTP or stdio transport.
-
-> New in schema v0.5.
->
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"mcp"`. |
-| **transport** | `string` | Transport protocol. One of: `"http"` (default), `"stdio"`. `"http"` exposes a Streamable HTTP server; `"stdio"` uses stdin/stdout JSON-RPC for local IDE integration. |
-| **address** | `string` | Server address. Can be a hostname, IPv4, or IPv6 address. |
-| **port** | `integer` | **REQUIRED when transport is `"http"`**. Port number (1–65535). MUST NOT be present when transport is `"stdio"`. |
-| **namespace** | `string` | **REQUIRED**. Unique identifier for this exposed MCP server. |
-| **description** | `string` | *Recommended*. A meaningful description of the MCP server's purpose. Sent as server instructions during MCP initialization. |
-| **tools** | `McpTool[]` | **REQUIRED**. List of MCP tools exposed by this server (minimum 1). |
-
-**Rules:**
-
-- The `type` field MUST be `"mcp"`.
-- The `namespace` field is mandatory and MUST be unique across all exposes entries.
-- The `tools` array is mandatory and MUST contain at least one entry.
-- When `transport` is `"http"` (or omitted, since `"http"` is the default), the `port` field is required.
-- When `transport` is `"stdio"`, the `port` field MUST NOT be present.
-- No additional properties are allowed.
-
-#### 3.5.5 McpTool Object
-
-An MCP tool definition. Each tool maps to one or more consumed HTTP operations, similar to ExposedOperation but adapted for the MCP protocol (no HTTP method, tool-oriented input schema).
-
-> The McpTool supports the same two modes as ExposedOperation: **simple** (direct `call` + `with`) and **orchestrated** (multi-step with `steps` + `mappings`).
->
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Technical name for the tool. Used as the MCP tool name. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **description** | `string` | **REQUIRED**. A meaningful description of the tool. Essential for agent discovery. |
-| **inputParameters** | `McpToolInputParameter[]` | Tool input parameters. These become the MCP tool's input schema (JSON Schema). |
-| **call** | `string` | **Simple mode only**. Reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. |
-| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. |
-| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. |
-| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the tool's output parameters. |
-| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. |
-| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. |
-
-**Modes:**
-
-**Simple mode** — direct call to a single consumed operation:
-
-- `call` is **REQUIRED**
-- `with` is optional
-- `outputParameters` are `MappedOutputParameter[]`
-- `steps` MUST NOT be present
-
-**Orchestrated mode** — multi-step orchestration:
-
-- `steps` is **REQUIRED** (at least 1 entry)
-- `mappings` is optional
-- `outputParameters` are `OrchestratedOutputParameter[]`
-- `call` and `with` MUST NOT be present
-
-**Rules:**
-
-- Both `name` and `description` are mandatory.
-- Exactly one of the two modes MUST be used (simple or orchestrated).
-- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation.
-- In orchestrated mode, the `steps` array MUST contain at least one entry.
-- The `$this` context reference works the same as for ExposedOperation: `$this.{mcpNamespace}.{paramName}` accesses the tool's input parameters.
-- No additional properties are allowed.
-
-#### 3.5.6 McpToolInputParameter Object
-
-Declares an input parameter for an MCP tool. These become properties in the tool's JSON Schema input definition.
-
-> Unlike `ExposedInputParameter`, MCP tool parameters have no `in` field (no HTTP location concept) and include a `required` flag.
->
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Parameter name. Becomes a property name in the tool's input schema. MUST match pattern `^[a-zA-Z0-9-_*]+$`. |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `integer`, `boolean`, `array`, `object`. |
-| **description** | `string` | **REQUIRED**. A meaningful description of the parameter. Used for agent discovery and tool documentation. |
-| **required** | `boolean` | Whether the parameter is required. Defaults to `true`. |
-
-**Rules:**
-
-- The `name`, `type`, and `description` fields are all mandatory.
-- The `type` field MUST be one of: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"array"`, `"object"`.
-- The `required` field defaults to `true` when omitted.
-- No additional properties are allowed.
-
-**McpToolInputParameter Example:**
-
-```yaml
-- name: database_id
- type: string
- description: The unique identifier of the Notion database
-- name: page_size
- type: number
- description: Number of results per page (max 100)
- required: false
-```
-
-#### 3.5.7 Address Validation Patterns
-
-- **Hostname**: `^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
-- **IPv4**: `^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
-- **IPv6**: `^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$`
-
-#### 3.5.8 Exposes Object Examples
-
-**API Expose with operations:**
-
-```yaml
-type: api
-port: 3000
-namespace: sample
-resources:
- - path: /status
- description: "Health check endpoint"
- name: health
- operations:
- - name: get-status
- method: GET
- call: api.health-check
- outputParameters:
- - type: string
- mapping: $.status
-```
-
-**API Expose with forward:**
-
-```yaml
-type: api
-port: 8080
-namespace: proxy
-resources:
- - path: /notion/{path}
- description: "Forward requests to the Notion API"
- forward:
- targetNamespace: notion
- trustedHeaders:
- - Notion-Version
-```
-
-**API Expose with both operations and forward:**
-
-```yaml
-type: api
-port: 9090
-namespace: hybrid
-resources:
- - path: /data/{path}
- description: "Resource with orchestrated operations and pass-through forwarding"
- operations:
- - name: get-summary
- method: GET
- call: api.get-summary
- forward:
- targetNamespace: api
- trustedHeaders:
- - Authorization
-```
-
-**MCP Expose with a single tool:**
-
-```yaml
-type: mcp
-port: 3001
-namespace: tools
-description: "AI-facing tools for database operations"
-tools:
- - name: get-database
- description: "Retrieve metadata about a database by its ID"
- inputParameters:
- - name: database_id
- type: string
- description: "The unique identifier of the database"
- call: api.get-database
- with:
- database_id: "$this.tools.database_id"
-```
-
----
-
-### 3.6 Consumes Object
-
-Describes a client adapter for consuming external APIs.
-
-> Update (schema v0.5): `targetUri` is now `baseUri`. The `headers` field has been removed — use `inputParameters` with `in: "header"` instead.
->
-
-#### 3.6.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Type of consumer. Valid values: `"http"`. |
-| **namespace** | `string` | Path suffix used for routing from exposes. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **baseUri** | `string` | **REQUIRED**. Base URI for the consumed API. Must be a valid http(s) URL (no `path` placeholder in the schema). |
-| **authentication** | Authentication Object | Authentication configuration. Defaults to `"inherit"`. |
-| **description** | `string` | *Recommended*. A description of the consumed API. The more meaningful it is, the easier for agent discovery. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters applied to all operations in this consumed API. |
-| **resources** | [ConsumedHttpResource Object] | **REQUIRED**. List of API resources. |
-
-#### 3.6.2 Rules
-
-- The `type` field MUST be `"http"`.
-- The `baseUri` field is required.
-- The `namespace` field is required and MUST be unique across all consumes entries.
-- The `namespace` value MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- The `description` field is recommended to improve agent discovery.
-- The `resources` array is required and MUST contain at least one entry.
-
-#### 3.6.3 Base URI Format
-
-The `baseUri` field MUST be a valid `http://` or `https://` URL, and may optionally include a base path.
-
-Example: `https://api.github.com` or `https://api.github.com/v3`
-
-#### 3.6.4 Consumes Object Example
-
-```yaml
-type: http
-namespace: github
-baseUri: https://api.github.com
-authentication:
- type: bearer
- token: "{{github_token}}"
-inputParameters:
- - name: Accept
- in: header
- value: "application/vnd.github.v3+json"
-resources:
- - name: users
- label: Users API
- path: /users/{username}
- operations:
- - name: get-user
- label: Get User
- method: GET
- inputParameters:
- - name: username
- in: path
- outputParameters:
- - name: userId
- type: string
- value: $.id
- - name: repos
- label: Repositories API
- path: /users/{username}/repos
- operations:
- - name: list-repos
- label: List Repositories
- method: GET
- inputParameters:
- - name: username
- in: path
- outputParameters:
- - name: repos
- type: array
- value: $
-```
-
----
-
-### 3.7 ConsumedHttpResource Object
-
-Describes an API resource that can be consumed from an external API.
-
-#### 3.7.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Technical name for the resource. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **label** | `string` | Display name of the resource. |
-| **description** | `string` | Description of the resource. |
-| **path** | `string` | **REQUIRED**. Path of the resource, relative to the consumes `baseUri`. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for this resource. |
-| **operations** | `ConsumedHttpOperation[]` | **REQUIRED**. List of operations for this resource. |
-
-#### 3.7.2 Rules
-
-- The `name` field MUST be unique within the parent consumes object's resources array.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- The `path` field will be appended to the parent consumes object's `baseUri`.
-- The `operations` array MUST contain at least one entry.
-- No additional properties are allowed.
-
-#### 3.7.3 ConsumedHttpResource Object Example
-
-```yaml
-name: users
-label: Users API
-path: /users/{username}
-inputParameters:
- - name: username
- in: path
-operations:
- - name: get-user
- label: Get User
- method: GET
- outputParameters:
- - name: userId
- type: string
- value: $.id
-```
-
----
-
-### 3.8 ConsumedHttpOperation Object
-
-Describes an operation that can be performed on a consumed resource.
-
-#### 3.8.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Technical name for the operation. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **label** | `string` | Display name of the operation. |
-| **description** | `string` | A longer description of the operation for documentation purposes. |
-| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. Default: `GET`. |
-| **inputParameters** | `ConsumedInputParameter[]` | Input parameters for the operation. |
-| **outputRawFormat** | `string` | The raw format of the response. One of: `json`, `xml`, `avro`, `protobuf`, `csv`, `yaml` . Default: `json`. |
-| **outputParameters** | `ConsumedOutputParameter[]` | Output parameters extracted from the response via JsonPath. |
-| **body** | `RequestBody` | Request body configuration. |
-
-#### 3.8.2 Rules
-
-- The `name` field MUST be unique within the parent resource's operations array.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-]+$` (alphanumeric and hyphens only).
-- Both `name` and `method` are mandatory.
-- No additional properties are allowed.
-
-#### 3.8.3 ConsumedHttpOperation Object Example
-
-```yaml
-name: get-user
-label: Get User Profile
-method: GET
-inputParameters:
- - name: username
- in: path
-outputParameters:
- - name: userId
- type: string
- value: $.id
- - name: username
- type: string
- value: $.login
- - name: email
- type: string
- value: $.email
-```
-
----
-
-### 3.9 ExposedOperation Object
-
-Describes an operation exposed on an exposed resource.
-
-> Update (schema v0.5): ExposedOperation now supports two modes via `oneOf` — **simple** (direct call with mapped output) and **orchestrated** (multi-step with named operation). The `call` and `with` fields are new. The `name` and `steps` fields are only required in orchestrated mode.
->
-
-#### 3.9.1 Fixed Fields
-
-All fields available on ExposedOperation:
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **method** | `string` | **REQUIRED**. HTTP method. One of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. |
-| **name** | `string` | Technical name for the operation (pattern `^[a-zA-Z0-9-]+$`). **REQUIRED in orchestrated mode only.** |
-| **label** | `string` | Display name for the operation (likely used in UIs). |
-| **description** | `string` | A longer description of the operation. Useful for agent discovery and documentation. |
-| **inputParameters** | `ExposedInputParameter[]` | Input parameters attached to the operation. |
-| **call** | `string` | **Simple mode only**. Direct reference to a consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. |
-| **with** | `WithInjector` | **Simple mode only**. Parameter injection for the called operation. |
-| **outputParameters** (simple) | `MappedOutputParameter[]` | **Simple mode**. Output parameters mapped from the consumed operation response. |
-| **steps** | `OperationStep[]` | **Orchestrated mode only. REQUIRED** (at least 1 step). Sequence of calls to consumed operations. |
-| **outputParameters** (orchestrated) | `OrchestratedOutputParameter[]` | **Orchestrated mode**. Output parameters with name and type. |
-| **mappings** | `StepOutputMapping[]` | **Orchestrated mode only**. Maps step outputs to the operation's output parameters at the operation level. |
-
-#### 3.9.2 Modes (oneOf)
-
-**Simple mode** — A direct call to a single consumed operation:
-
-- `call` is **REQUIRED**
-- `with` is optional (inject parameters into the call)
-- `outputParameters` are `MappedOutputParameter[]` (type + mapping)
-- `steps` MUST NOT be present
-- `name` is optional
-
-**Orchestrated mode** — A multi-step orchestration:
-
-- `name` is **REQUIRED**
-- `steps` is **REQUIRED** (at least 1 entry)
-- `mappings` is optional — maps step outputs to the operation's output parameters at the operation level
-- `outputParameters` are `OrchestratedOutputParameter[]` (name + type)
-- `call` and `with` MUST NOT be present
-
-#### 3.9.3 Rules
-
-- Exactly one of the two modes MUST be used (simple or orchestrated).
-- In simple mode, `call` MUST follow the format `{namespace}.{operationId}` and reference a valid consumed operation.
-- In orchestrated mode, the `steps` array MUST contain at least one entry. Each step references a consumed operation using `{namespace}.{operationName}`.
-- The `method` field is always required regardless of mode.
-
-#### 3.9.4 ExposedOperation Object Examples
-
-**Simple mode (direct call):**
-
-```yaml
-method: GET
-label: Get User Profile
-call: github.get-user
-with:
- username: $this.sample.username
-outputParameters:
- - type: string
- mapping: $.login
- - type: number
- mapping: $.id
-```
-
-**Orchestrated mode (multi-step):**
-
-```yaml
-name: get-db
-method: GET
-label: Get Database
-inputParameters:
- - name: database_id
- in: path
- type: string
- description: The ID of the database to retrieve
-steps:
- - type: call
- name: fetch-db
- call: notion.get-database
- with:
- database_id: "$this.sample.database_id"
-mappings:
- - targetName: db_name
- value: "$.dbName"
-outputParameters:
- - name: db_name
- type: string
- - name: Api-Version
- type: string
-```
-
----
-
-### 3.10 RequestBody Object
-
-Describes request body configuration for consumed operations. `RequestBody` is a `oneOf` — exactly one of five subtypes must be used.
-
-#### 3.10.1 Subtypes
-
-**RequestBodyJson** — JSON body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"json"`. |
-| **data** | `string` | `object` | `array` | **REQUIRED**. The JSON payload. Can be a raw JSON string, an inline object, or an array. |
-
-**RequestBodyText** — Plain text, XML, or SPARQL body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. One of: `"text"`, `"xml"`, `"sparql"`. |
-| **data** | `string` | **REQUIRED**. The text payload. |
-
-**RequestBodyFormUrlEncoded** — URL-encoded form body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"formUrlEncoded"`. |
-| **data** | `string` | `object` | **REQUIRED**. Either a raw URL-encoded string or an object whose values are strings. |
-
-**RequestBodyMultipartForm** — Multipart form body
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"multipartForm"`. |
-| **data** | `RequestBodyMultipartFormPart[]` | **REQUIRED**. Array of form parts. Each part has: `name` (required), `value` (required), `filename` (optional), `contentType` (optional). |
-
-**RequestBodyRaw** — Raw string body
-
-`RequestBody` can also be a plain `string`. When used with a YAML block scalar (`|`), the string is sent as-is. Interpreted as JSON by default.
-
-#### 3.10.2 Rules
-
-- Exactly one of the five subtypes must be used.
-- For structured subtypes, both `type` and `data` are mandatory.
-- No additional properties are allowed on any subtype.
-
-#### 3.10.3 RequestBody Examples
-
-**JSON body (object):**
-
-```yaml
-body:
- type: json
- data:
- hello: "world"
-```
-
-**JSON body (string):**
-
-```yaml
-body:
- type: json
- data: '{"key": "value"}'
-```
-
-**Text body:**
-
-```yaml
-body:
- type: text
- data: "Hello, world!"
-```
-
-**Form URL-encoded body:**
-
-```yaml
-body:
- type: formUrlEncoded
- data:
- username: "admin"
- password: "secret"
-```
-
-**Multipart form body:**
-
-```yaml
-body:
- type: multipartForm
- data:
- - name: "file"
- value: "base64content..."
- filename: "document.pdf"
- contentType: "application/pdf"
- - name: "description"
- value: "My uploaded file"
-```
-
-**Raw body:**
-
-```yaml
-body: |
- {
- "filter": {
- "property": "Status",
- "select": { "equals": "Active" }
- }
- }
-```
-
----
-
-### 3.11 InputParameter Objects
-
-> Update (schema v0.5): The single `InputParameter` object has been split into two distinct types: **ConsumedInputParameter** (used in consumes) and **ExposedInputParameter** (used in exposes, with additional `type` and `description` fields required).
->
-
-#### 3.11.1 ConsumedInputParameter Object
-
-Used in consumed resources and operations.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. |
-| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. |
-| **value** | `string` | Value or JSONPath reference. |
-
-**Rules:**
-
-- Both `name` and `in` are mandatory.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`.
-- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`.
-- A unique parameter is defined by the combination of `name` and `in`.
-- No additional properties are allowed.
-
-**ConsumedInputParameter Example:**
-
-```yaml
-- name: username
- in: path
-- name: page
- in: query
-- name: Authorization
- in: header
- value: "Bearer token"
-```
-
-#### 3.11.2 ExposedInputParameter Object
-
-Used in exposed resources and operations. Extends the consumed variant with `type` (required) and `description` (recommended) for agent discoverability, plus an optional `pattern` for validation.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Parameter name. MUST match pattern `^[a-zA-Z0-9-*]+$`. |
-| **in** | `string` | **REQUIRED**. Parameter location. Valid values: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`. |
-| **type** | `string` | **REQUIRED**. Data type of the parameter. One of: `string`, `number`, `integer`, `boolean`, `object`, `array`. |
-| **description** | `string` | *Recommended*. Human-readable description of the parameter. Provides valuable context for agent discovery. |
-| **pattern** | `string` | Optional regex pattern for parameter value validation. |
-| **value** | `string` | Default value or JSONPath reference. |
-
-**Rules:**
-
-- The `name`, `in`, and `type` fields are mandatory. The `description` field is recommended for agent discovery.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-*]+$`.
-- The `in` field MUST be one of: `"query"`, `"header"`, `"path"`, `"cookie"`, `"body"`.
-- The `type` field MUST be one of: `"string"`, `"number"`, `"integer"`, `"boolean"`, `"object"`, `"array"`.
-- No additional properties are allowed.
-
-**ExposedInputParameter Example:**
-
-```yaml
-- name: database_id
- in: path
- type: string
- description: The unique identifier of the Notion database
- pattern: "^[a-f0-9-]+$"
-- name: page_size
- in: query
- type: number
- description: Number of results per page (max 100)
-```
-
----
-
-### 3.12 OutputParameter Objects
-
-> Update (schema v0.5): The single `OutputParameter` object has been split into three distinct types: **ConsumedOutputParameter** (used in consumed operations), **MappedOutputParameter** (used in simple-mode exposed operations), and **OrchestratedOutputParameter** (used in orchestrated-mode exposed operations).
->
-
-#### 3.12.1 ConsumedOutputParameter Object
-
-Used in consumed operations to extract values from the raw API response.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Output parameter name. MUST match pattern `^[a-zA-Z0-9-_]+$`. |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-| **value** | `string` | **REQUIRED**. JsonPath expression to extract value from consumed function response. |
-
-**Rules:**
-
-- All three fields (`name`, `type`, `value`) are mandatory.
-- The `name` field MUST match the pattern `^[a-zA-Z0-9-_*]+$`.
-- The `value` field MUST start with `$`.
-- No additional properties are allowed.
-
-**ConsumedOutputParameter Example:**
-
-```yaml
-outputParameters:
- - name: dbName
- type: string
- value: $.title[0].text.content
- - name: dbId
- type: string
- value: $.id
-```
-
-#### 3.12.2 MappedOutputParameter Object
-
-Used in **simple mode** exposed operations. Maps a value from the consumed response using `type` and `mapping`.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-| **mapping** | `string` | `object` | **REQUIRED**. For scalar types (`string`, `number`, `boolean`): a JsonPath string. For `object`: an object with `properties` (recursive MappedOutputParameter map). For `array`: an object with `items` (recursive MappedOutputParameter). |
-
-**Subtypes by type:**
-
-- **`string`**, **`number`**, **`boolean`**: `mapping` is a JSONPath string (e.g. `$.login`)
-- **`object`**: `mapping` is `{ properties: { key: MappedOutputParameter, ... } }` — recursive
-- **`array`**: `mapping` is `{ items: MappedOutputParameter }` — recursive
-
-**Rules:**
-
-- Both `type` and `mapping` are mandatory.
-- No additional properties are allowed.
-
-**MappedOutputParameter Examples:**
-
-```yaml
-# Scalar mapping
-outputParameters:
- - type: string
- mapping: $.login
- - type: number
- mapping: $.id
-
-# Object mapping (recursive)
-outputParameters:
- - type: object
- mapping:
- properties:
- username:
- type: string
- mapping: $.login
- userId:
- type: number
- mapping: $.id
-
-# Array mapping (recursive)
-outputParameters:
- - type: array
- mapping:
- items:
- type: object
- mapping:
- properties:
- name:
- type: string
- mapping: $.name
-```
-
-#### 3.12.3 OrchestratedOutputParameter Object
-
-Used in **orchestrated mode** exposed operations. Declares an output by `name` and `type` (the value is populated via step mappings).
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Output parameter name. |
-| **type** | `string` | **REQUIRED**. Data type. One of: `string`, `number`, `boolean`, `object`, `array`. |
-
-**Subtypes by type:**
-
-- **`string`**, **`number`**, **`boolean`** (scalar): only `name` and `type`
-- **`array`**: adds `items` (recursive OrchestratedOutputParameter without `name`)
-- **`object`**: adds `properties` (map of name → recursive OrchestratedOutputParameter without `name`)
-
-**Rules:**
-
-- Both `name` and `type` are mandatory.
-- No additional properties are allowed.
-
-**OrchestratedOutputParameter Example:**
-
-```yaml
-outputParameters:
- - name: db_name
- type: string
- - name: Api-Version
- type: string
- - name: results
- type: array
- items:
- type: object
- properties:
- id:
- type: string
- title:
- type: string
-```
-
-#### 3.12.4 JSONPath roots (extensions)
-
-In a consumed resource, **`$`** refers to the *raw response payload* of the consumed operation (after decoding based on `outputRawFormat`). The root `$` gives direct access to the JSON response body.
-
-Example, if you consider the following JSON response :
-
-```json
-{
- "id": "154548",
- "titles": [
- {
- "text": {
- "content": "This is title[0].text.content",
- "author": "user1"
- }
- }
- ],
- "created_time": "2024-06-01T12:00:00Z"
-}
-```
-
-- `$.id` is `154548`
-- `$.titles[0].text.content` is `This is title[0].text.content`
-
-#### 3.12.5 Common patterns
-
-- `$.fieldName` — accesses a top-level field
-- `$.data.user.id` — accesses nested fields
-- `$.items[0]` — accesses array elements
-- `$.items[*].id` — accesses all ids in an array
-
----
-
-### 3.13 OperationStep Object
-
-Describes a single step in an orchestrated operation. `OperationStep` is a `oneOf` between two subtypes: **OperationStepCall** and **OperationStepLookup**, both sharing a common **OperationStepBase**.
-
-> Update (schema v0.5): OperationStep is now a discriminated union (`oneOf`) with a required `type` field (`"call"` or `"lookup"`) and a required `name` field. `OperationStepCall` uses `with` (WithInjector) instead of `inputParameters`. `OperationStepLookup` is entirely new.
->
-
-#### 3.13.1 OperationStepBase (shared fields)
-
-All operation steps share these base fields:
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. Step type discriminator. One of: `"call"`, `"lookup"`. |
-| **name** | `string` | **REQUIRED**. Technical name for the step (pattern `^[a-zA-Z0-9-]+$`). Used as namespace for referencing step outputs in mappings and expressions. |
-
-#### 3.13.2 OperationStepCall
-
-Calls a consumed operation.
-
-**Fixed Fields** (in addition to base):
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"call"`. |
-| **name** | `string` | **REQUIRED**. Step name (from base). |
-| **call** | `string` | **REQUIRED**. Reference to consumed operation. Format: `{namespace}.{operationId}`. MUST match pattern `^[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+$`. |
-| **with** | `WithInjector` | Parameter injection for the called operation. Keys are parameter names, values are strings or numbers (static values or `$this` references). |
-
-**Rules:**
-
-- `type`, `name`, and `call` are mandatory.
-- The `call` field MUST follow the format `{namespace}.{operationName}`.
-- The `namespace` portion MUST correspond to a namespace defined in one of the capability's consumes entries.
-- The `operationName` portion MUST correspond to an operation `name` defined in the consumes entry identified by the namespace.
-- `with` uses the same `WithInjector` object as simple-mode ExposedOperation (see §3.18).
-- No additional properties are allowed.
-
-#### 3.13.3 OperationStepLookup
-
-Performs a lookup against the output of a previous call step, matching values and extracting fields.
-
-**Fixed Fields** (in addition to base):
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"lookup"`. |
-| **name** | `string` | **REQUIRED**. Step name (from base). |
-| **index** | `string` | **REQUIRED**. Name of a previous call step whose output serves as the lookup table. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **match** | `string` | **REQUIRED**. Name of the key field in the index to match against. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **lookupValue** | `string` | **REQUIRED**. JSONPath expression resolving to the value(s) to look up. |
-| **outputParameters** | `string[]` | **REQUIRED**. List of field names to extract from the matched index entries (minimum 1 entry). |
-
-**Rules:**
-
-- `type`, `name`, `index`, `match`, `lookupValue`, and `outputParameters` are all mandatory.
-- `outputParameters` MUST contain at least one entry.
-- The `index` value MUST reference the `name` of a previous `call` step in the same orchestration.
-- No additional properties are allowed.
-
-#### 3.13.4 Call Reference Resolution
-
-The `call` value on an `OperationStepCall` is resolved as follows:
-
-1. Split the value on the `.` character into namespace and operationName
-2. Find the consumes entry with matching `namespace` field
-3. Within that consumes entry's resources, find the operation with matching `name` field
-4. Execute that operation as part of the orchestration sequence
-
-#### 3.13.5 OperationStep Object Examples
-
-**Call step with parameter injection:**
-
-```yaml
-steps:
- - type: call
- name: fetch-db
- call: notion.get-database
- with:
- database_id: $this.sample.database_id
-```
-
-**Lookup step (match against a previous call's output):**
-
-```yaml
-steps:
- - type: call
- name: list-users
- call: github.list-users
- - type: lookup
- name: find-user
- index: list-users
- match: email
- lookupValue: $this.sample.user_email
- outputParameters:
- - login
- - id
-```
-
-**Multi-step orchestration (call + lookup):**
-
-```yaml
-steps:
- - type: call
- name: get-entries
- call: api.list-entries
- - type: lookup
- name: resolve-entry
- index: get-entries
- match: entry_id
- lookupValue: $this.sample.target_id
- outputParameters:
- - title
- - status
- - type: call
- name: post-result
- call: slack.post-message
- with:
- text: $this.sample.title
-```
-
----
-
-### 3.14 StepOutputMapping Object
-
-Describes how to map the output of an operation step to the input of another step or to the output of the exposed operation.
-
-#### 3.14.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **targetName** | `string` | **REQUIRED**. The name of the parameter to map to. It can be an input parameter of a next step or an output parameter of the exposed operation. |
-| **value** | `string` | **REQUIRED**. A JSONPath reference to the value to map from. E.g. `$.get-database.database_id`. |
-
-#### 3.14.2 Rules
-
-- Both `targetName` and `value` are mandatory.
-- No additional properties are allowed.
-
-#### 3.14.3 How mappings wire steps to exposed outputs
-
-A StepOutputMapping connects the **output parameters of a consumed operation** (called by the step) to the **output parameters of the exposed operation** (or to input parameters of subsequent steps).
-
-- **`targetName`** — refers to the `name` of an output parameter declared on the exposed operation, or the `name` of an input parameter of a subsequent step. The target parameter receives its value from the mapping.
-- **`value`** — a JSONPath expression where **`$`** is the root of the consumed operation's output parameters. The syntax `$.{outputParameterName}` references a named output parameter of the consumed operation called in this step.
-
-#### 3.14.4 End-to-end example
-
-Consider a consumed operation `notion.get-database` that declares:
-
-```yaml
-# In consumes → resources → operations
-name: "get-database"
-outputParameters:
- - name: "dbName"
- value: "$.title[0].text.content"
-```
-
-And the exposed side of the capability:
-
-```yaml
-# In exposes
-exposes:
- - type: "api"
- address: "localhost"
- port: 9090
- namespace: "sample"
- resources:
- - path: "/databases/{database_id}"
- name: "db"
- label: "Database resource"
- description: "Retrieve information about a Notion database"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "The unique identifier of the Notion database"
- operations:
- - name: "get-db"
- method: "GET"
- label: "Get Database"
- outputParameters:
- - name: "db_name"
- type: "string"
- steps:
- - type: "call"
- name: "fetch-db"
- call: "notion.get-database"
- with:
- database_id: "$this.sample.database_id"
- mappings:
- - targetName: "db_name"
- value: "$.dbName"
-```
-
-Here is what happens at orchestration time:
-
-1. The step `fetch-db` calls `notion.get-database`, which extracts `dbName` and `dbId` from the raw response via its own output parameters.
-2. The `with` injector passes `database_id` from the exposed input parameter (`$this.sample.database_id`) to the consumed operation.
-3. The mapping `targetName: "db_name"` refers to the exposed operation's output parameter `db_name`.
-4. The mapping `value: "$.dbName"` resolves to the value of the consumed operation's output parameter named `dbName`.
-5. As a result, the exposed output `db_name` is populated with the value extracted by `$.dbName` (i.e. `title[0].text.content` from the raw Notion API response).
-
-#### 3.14.5 StepOutputMapping Object Example
-
-```yaml
-mappings:
- - targetName: "db_name"
- value: "$.dbName"
-```
-
----
-
-### 3.15 `$this` Context Reference
-
-Describes how `$this` references work in `with` (WithInjector) and other expression contexts.
-
-> Update (schema v0.5): The former `OperationStepParameter` object (with `name` and `value` fields) has been replaced by `WithInjector` (see §3.18). This section now documents the `$this` expression root, which is used within `WithInjector` values.
->
-
-#### 3.15.1 The `$this` root
-
-In a `with` (WithInjector) value — whether on an ExposedOperation (simple mode) or an OperationStepCall — the **`$this`** root references the *current capability execution context*, i.e. values already resolved during orchestration.
-
-**`$this`** navigates the expose layer's input parameters using the path `$this.{exposeNamespace}.{inputParameterName}`. This allows a step or a simple-mode call to receive values that were provided by the caller of the exposed operation.
-
-- **`$this.{exposeNamespace}.{paramName}`** — accesses an input parameter of the exposed resource or operation identified by its namespace.
-- The `{exposeNamespace}` corresponds to the `namespace` of the exposed API.
-- The `{paramName}` corresponds to the `name` of an input parameter declared on the exposed resource or operation.
-
-#### 3.15.2 Example
-
-If the exposed API has namespace `sample` and an input parameter `database_id` declared on its resource, then:
-
-- `$this.sample.database_id` resolves to the value of `database_id` provided by the caller.
-
-**Usage in a WithInjector:**
-
-```yaml
-call: notion.get-database
-with:
- database_id: $this.sample.database_id
-```
-
----
-
-### 3.16 Authentication Object
-
-Defines authentication configuration. Four types are supported: basic, apikey, bearer, and digest.
-
-#### 3.16.1 Basic Authentication
-
-HTTP Basic Authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"basic"`. |
-| **username** | `string` | Username for basic auth. |
-| **password** | `string` | Password for basic auth. |
-
-**Example:**
-
-```yaml
-authentication:
- type: basic
- username: admin
- password: "secret_password"
-```
-
-#### 3.16.2 API Key Authentication
-
-API Key authentication via header or query parameter.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"apikey"`. |
-| **key** | `string` | API key name (header name or query parameter name). |
-| **value** | `string` | API key value. |
-| **placement** | `string` | Where to place the key. Valid values: `"header"`, `"query"`. |
-
-**Example:**
-
-```yaml
-authentication:
- type: apikey
- key: X-API-Key
- value: "{{api_key}}"
- placement: header
-```
-
-#### 3.16.3 Bearer Token Authentication
-
-Bearer token authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"bearer"`. |
-| **token** | `string` | Bearer token value. |
-
-**Example:**
-
-```yaml
-authentication:
- type: bearer
- token: "bearer_token"
-```
-
-#### 3.16.4 Digest Authentication
-
-HTTP Digest Authentication.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **type** | `string` | **REQUIRED**. MUST be `"digest"`. |
-| **username** | `string` | Username for digest auth. |
-| **password** | `string` | Password for digest auth. |
-
-**Example:**
-
-```yaml
-authentication:
- type: digest
- username: admin
- password: "secret_password"
-```
-
-#### 3.16.5 Rules
-
-- Only one authentication type can be used per authentication object.
-- The `type` field determines which additional fields are required or allowed.
-- Authentication can be specified at multiple levels (exposes, consumes) with inner levels overriding outer levels.
-
----
-
-### 3.17 ForwardConfig Object
-
-Defines forwarding configuration for an exposed resource to pass requests through to a consumed namespace.
-
-> Update (schema v0.5): Renamed from `ForwardHeaders` to `ForwardConfig`. The `targetNamespaces` array has been replaced by a single `targetNamespace` string.
->
-
-#### 3.17.1 Fixed Fields
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **targetNamespace** | `string` | **REQUIRED**. The consumer namespace to forward requests to. MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **trustedHeaders** | [`string`] | **REQUIRED**. List of headers allowed to be forwarded (minimum 1 entry). No wildcards supported. |
-
-#### 3.17.2 Rules
-
-- The `targetNamespace` field is mandatory and MUST reference a valid namespace from one of the capability's consumes entries.
-- The `trustedHeaders` array is mandatory and MUST contain at least one entry.
-- Header names in `trustedHeaders` are case-insensitive (following HTTP header conventions).
-- Only headers listed in `trustedHeaders` will be forwarded to the consumed source.
-- No additional properties are allowed.
-
-#### 3.17.3 ForwardConfig Object Example
-
-```yaml
-forward:
- targetNamespace: notion
- trustedHeaders:
- - Authorization
- - Notion-Version
-```
-
----
-
-### 3.18 WithInjector Object
-
-Defines parameter injection for simple-mode exposed operations. Used with the `with` field on an ExposedOperation to inject values into the called consumed operation.
-
-> New in schema v0.5.
->
-
-#### 3.18.1 Shape
-
-`WithInjector` is an object whose keys are parameter names and whose values are static values or `$this` references.
-
-- Each key corresponds to a parameter `name` in the consumed operation's `inputParameters`.
-- Each value is a `string` or a `number`: either a static value or a `$this.{namespace}.{paramName}` reference.
-
-#### 3.18.2 Rules
-
-- The keys MUST correspond to valid parameter names in the consumed operation being called.
-- Values can be strings or numbers.
-- String values can use the `$this` root to reference exposed input parameters (same as in OperationStepParameter).
-- No additional constraints.
-
-#### 3.18.3 WithInjector Object Example
-
-```yaml
-call: github.get-user
-with:
- username: $this.sample.username
- Accept: "application/json"
- maxRetries: 3
-```
-
----
-
-### 3.19 ExternalRef Object
-
-> **Updated**: ExternalRef is now a discriminated union (`oneOf`) with two variants — **file-resolved** (for local development) and **runtime-resolved** (for production). Variables are explicitly declared via a `keys` map.
->
-
-Declares an external reference that provides variables to the capability. External references are declared at the root level of the Naftiko document via the `externalRefs` array.
-
-`ExternalRef` is a `oneOf` — exactly one of the two variants must be used.
-
-#### 3.19.1 File-Resolved ExternalRef
-
-Loads variables from a local file. Intended for **local development only**.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **description** | `string` | *Recommended*. Used to provide *meaningful* information about the external reference. In a world of agents, context is king. |
-| **type** | `string` | **REQUIRED**. MUST be `"environment"`. |
-| **resolution** | `string` | **REQUIRED**. MUST be `"file"`. |
-| **uri** | `string` | **REQUIRED**. URI pointing to the file (e.g. `file:///path/to/env.json`). |
-| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the resolved file content. |
-
-**Rules:**
-
-- The `name`, `type`, `resolution`, `uri`, and `keys` fields are mandatory. The `description` field is recommended.
-- No additional properties are allowed.
-
-#### 3.19.2 Runtime-Resolved ExternalRef
-
-Variables are injected by the execution environment at startup (default). The capability document does **not** specify where the values come from — this is delegated to the deployment platform.
-
-**Fixed Fields:**
-
-| Field Name | Type | Description |
-| --- | --- | --- |
-| **name** | `string` | **REQUIRED**. Unique identifier (kebab-case). MUST match pattern `^[a-zA-Z0-9-]+$`. |
-| **type** | `string` | **REQUIRED**. MUST be `"environment"`. |
-| **resolution** | `string` | **REQUIRED.** MUST be `"runtime"`. |
-| **keys** | `ExternalRefKeys` | **REQUIRED**. Map of variable names to keys in the runtime context. |
-
-**Rules:**
-
-- `name`, `type`, and `keys` are mandatory.
-- `resolution` is optional; when present MUST be `"runtime"`.
-- No additional properties are allowed.
-
-Typical production providers include:
-
-- **HashiCorp Vault** — centralized secrets management
-- **Kubernetes Secrets** / **ConfigMaps** — native K8s secret injection
-- **AWS Secrets Manager** / **AWS SSM Parameter Store**
-- **Azure Key Vault**
-- **GCP Secret Manager**
-- **Docker Secrets** — for containerized deployments
-- **CI/CD pipeline variables** (GitHub Actions secrets, GitLab CI variables, etc.)
-
-#### 3.19.3 ExternalRefKeys Object
-
-A map of key-value pairs that define the variables to be injected from the external reference.
-
-- Each **key** is the variable name used for injection (available as `\{\{key\}\}` in the capability definition)
-- Each **value** is the corresponding key in the resolved file content or runtime context
-
-Example: `{"notion_token": "NOTION_INTEGRATION_TOKEN"}` means the value of `NOTION_INTEGRATION_TOKEN` in the source will be injected as `{{notion_token}}` in the capability definition.
-
-**Schema:**
-
-```json
-{
- "type": "object",
- "additionalProperties": { "type": "string" }
-}
-```
-
-#### 3.19.4 Rules
-
-- Each `name` value MUST be unique across all `externalRefs` entries.
-- The `name` value MUST NOT collide with any `consumes` namespace to avoid ambiguity.
-- The `keys` map MUST contain at least one entry.
-- Variable names (keys in the `keys` map) SHOULD be unique across all `externalRefs` entries. If the same variable name appears in multiple entries, the expression MUST use the qualified form `{{name.variable}}` (where `name` is the `name` of the `externalRefs` entry) to disambiguate which source provides the value.
-- No additional properties are allowed on either variant.
-
-
-
-#### 3.19.5 ExternalRef Object Examples
-
-**File resolution (development):**
-
-```yaml
-externalRefs:
- - name: "notion-env"
- type: "environment"
- description: "External reference to Notion API for accessing project data stored in Notion."
- resolution: file
- uri: "file:///path/to/notion_env.json"
- keys:
- notion_token: "NOTION_INTEGRATION_TOKEN"
- notion_projects_db_id: "PROJECTS_DATABASE_ID"
- notion_time_tracker_db_id: "TIME_TRACKER_DATABASE_ID"
-```
-
-**Runtime resolution (production):**
-
-```yaml
-externalRefs:
- - name: "secrets"
- type: "environment"
- resolution: runtime
- keys:
- notion_token: "NOTION_INTEGRATION_TOKEN"
- github_token: "GITHUB_TOKEN"
-```
-
-**Minimal runtime (resolution omitted — defaults to runtime):**
-
-```yaml
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- api_key: "API_KEY"
-```
-
----
-
-### 3.20 Expression Syntax
-
-Variables declared in `externalRefs` via the `keys` map are injected into the capability document using mustache-style `\{\{variable\}\}` expressions.
-
-#### 3.20.1 Format
-
-The expression format is `\{\{key\}\}`, where `key` is a variable name declared in the `keys` map of an `externalRefs` entry.
-
-Expressions can appear in any `string` value within the document, including authentication tokens, header values, and input parameter values.
-
-#### 3.20.2 Resolution
-
-At runtime, expressions are resolved as follows:
-
-1. Find the `externalRefs` entry whose `keys` map contains the referenced variable name
-2. Look up the corresponding source key in the `keys` map
-3. Resolve the source key value using the strategy defined by `resolution` (`file` lookup or `runtime` injection)
-4. Replace the `\{\{variable\}\}` expression with the resolved value
-
-If a referenced variable is not declared in any `externalRefs` entry's `keys`, the document MUST be considered invalid.
-
-#### 3.20.3 Relationship with `$this`
-
-`\{\{variable\}\}` expressions and `$this` references serve different purposes:
-
-- `\{\{variable\}\}` resolves **static configuration** from external references (secrets, environment variables) declared via `keys`
-- `$this.{exposeNamespace}.{paramName}` resolves **runtime orchestration** values from the expose layer's input parameters
-
-The two expression systems are independent and MUST NOT be mixed.
-
-#### 3.20.4 Expression Examples
-
-```yaml
-# Authentication token from external ref
-authentication:
- type: bearer
- token: "{{notion_token}}"
-
-# Input parameter with header value from external ref
-inputParameters:
- - name: Notion-Version
- in: header
- value: "{{notion_version}}"
-
-# Corresponding externalRefs declaration
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
- api_key: "API_KEY"
-```
-
----
-
-## 4. Complete Examples
-
-This section provides progressive examples — from the simplest capability to a full-featured one — to illustrate the main patterns of the specification. All examples are pseudo-functional and use realistic API shapes.
-
-### 4.1 Forward-only capability (proxy)
-
-The simplest capability: forward incoming requests to a consumed API without any transformation.
-
-```yaml
----
-naftiko: "0.5"
-info:
- label: "Notion Proxy"
- description: "Pass-through proxy to the Notion API for development and debugging"
- tags:
- - proxy
- - notion
- created: "2026-02-01"
- modified: "2026-02-01"
-
-capability:
- exposes:
- - type: "api"
- port: 8080
- namespace: "proxy"
- resources:
- - path: "/notion/{path}"
- description: "Forwards all requests to the Notion API"
- forward:
- targetNamespace: "notion"
- trustedHeaders:
- - "Authorization"
- - "Notion-Version"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API"
- baseUri: "https://api.notion.com/v1"
- resources:
- - name: "all"
- path: "/{path}"
- operations:
- - name: "any"
- method: "GET"
-```
-
-### 4.2 Simple-mode capability (direct call)
-
-A single exposed operation that directly calls a consumed operation, maps parameters with `with`, and extracts output.
-
-```yaml
----
-naftiko: "0.5"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- github_token: "GITHUB_TOKEN"
-info:
- label: "GitHub User Lookup"
- description: "Exposes a simplified endpoint to retrieve GitHub user profiles"
- tags:
- - github
- - users
- created: "2026-02-01"
- modified: "2026-02-01"
-
-capability:
- exposes:
- - type: "api"
- port: 3000
- namespace: "app"
- resources:
- - path: "/users/{username}"
- description: "Look up a GitHub user by username"
- name: "user"
- inputParameters:
- - name: "username"
- in: "path"
- type: "string"
- description: "The GitHub username to look up"
- operations:
- - method: "GET"
- label: "Get User"
- call: "github.get-user"
- with:
- username: "$this.app.username"
- outputParameters:
- - type: "string"
- mapping: "$.login"
- - type: "string"
- mapping: "$.email"
- - type: "number"
- mapping: "$.id"
-
- consumes:
- - type: "http"
- namespace: "github"
- description: "GitHub REST API v3"
- baseUri: "https://api.github.com"
- authentication:
- type: "bearer"
- token: "{{github_token}}"
- resources:
- - name: "users"
- path: "/users/{username}"
- label: "Users"
- operations:
- - name: "get-user"
- label: "Get User"
- method: "GET"
- inputParameters:
- - name: "username"
- in: "path"
- outputParameters:
- - name: "login"
- type: "string"
- value: "$.login"
- - name: "email"
- type: "string"
- value: "$.email"
- - name: "id"
- type: "number"
- value: "$.id"
-```
-
-### 4.3 Orchestrated capability (multi-step call)
-
-An exposed operation that chains two consumed operations using named steps and `with`.
-
-```yaml
----
-naftiko: "0.5"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
-info:
- label: "Database Inspector"
- description: "Retrieves a Notion database then queries its contents in a single exposed operation"
- tags:
- - notion
- - orchestration
- created: "2026-02-10"
- modified: "2026-02-10"
-
-capability:
- exposes:
- - type: "api"
- port: 9090
- namespace: "inspector"
- resources:
- - path: "/databases/{database_id}/summary"
- description: "Returns database metadata and first page of results"
- name: "db-summary"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "The Notion database ID"
- operations:
- - name: "get-summary"
- method: "GET"
- label: "Get Database Summary"
- steps:
- - type: "call"
- name: "fetch-db"
- call: "notion.get-database"
- with:
- database_id: "$this.inspector.database_id"
- - type: "call"
- name: "query-db"
- call: "notion.query-database"
- with:
- database_id: "$this.inspector.database_id"
- mappings:
- - targetName: "db_name"
- value: "$.fetch-db.dbName"
- - targetName: "row_count"
- value: "$.query-db.resultCount"
- outputParameters:
- - name: "db_name"
- type: "string"
- - name: "row_count"
- type: "number"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API"
- baseUri: "https://api.notion.com/v1"
- authentication:
- type: "bearer"
- token: "{{notion_token}}"
- inputParameters:
- - name: "Notion-Version"
- in: "header"
- value: "2022-06-28"
- resources:
- - name: "databases"
- path: "/databases/{database_id}"
- label: "Databases"
- operations:
- - name: "get-database"
- label: "Get Database"
- method: "GET"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "dbName"
- type: "string"
- value: "$.title[0].text.content"
- - name: "dbId"
- type: "string"
- value: "$.id"
- - name: "queries"
- path: "/databases/{database_id}/query"
- label: "Database queries"
- operations:
- - name: "query-database"
- label: "Query Database"
- method: "POST"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "resultCount"
- type: "number"
- value: "$.results.length()"
- - name: "results"
- type: "array"
- value: "$.results"
-```
-
-### 4.4 Orchestrated capability with lookup step
-
-Demonstrates a `lookup` step that cross-references the output of a previous call to enrich data.
-
-```yaml
----
-naftiko: "0.5"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- hr_api_key: "HR_API_KEY"
-info:
- label: "Team Member Resolver"
- description: "Resolves team member details by matching email addresses from a project tracker"
- tags:
- - hr
- - lookup
- created: "2026-02-15"
- modified: "2026-02-15"
-
-capability:
- exposes:
- - type: "api"
- port: 4000
- namespace: "team"
- resources:
- - path: "/resolve/{email}"
- description: "Finds a team member by email and returns their profile"
- name: "resolve"
- inputParameters:
- - name: "email"
- in: "path"
- type: "string"
- description: "Email address to look up"
- operations:
- - name: "resolve-member"
- method: "GET"
- label: "Resolve Team Member"
- steps:
- - type: "call"
- name: "list-members"
- call: "hr.list-employees"
- - type: "lookup"
- name: "find-member"
- index: "list-members"
- match: "email"
- lookupValue: "$this.team.email"
- outputParameters:
- - "fullName"
- - "department"
- - "role"
- mappings:
- - targetName: "name"
- value: "$.find-member.fullName"
- - targetName: "department"
- value: "$.find-member.department"
- - targetName: "role"
- value: "$.find-member.role"
- outputParameters:
- - name: "name"
- type: "string"
- - name: "department"
- type: "string"
- - name: "role"
- type: "string"
-
- consumes:
- - type: "http"
- namespace: "hr"
- description: "Internal HR system API"
- baseUri: "https://hr.internal.example.com/api"
- authentication:
- type: "apikey"
- key: "X-Api-Key"
- value: "{{hr_api_key}}"
- placement: "header"
- resources:
- - name: "employees"
- path: "/employees"
- label: "Employees"
- operations:
- - name: "list-employees"
- label: "List All Employees"
- method: "GET"
- outputParameters:
- - name: "email"
- type: "string"
- value: "$.items[*].email"
- - name: "fullName"
- type: "string"
- value: "$.items[*].name"
- - name: "department"
- type: "string"
- value: "$.items[*].department"
- - name: "role"
- type: "string"
- value: "$.items[*].role"
-```
-
-### 4.5 Full-featured capability (mixed modes)
-
-Combines forward proxy, simple-mode operations, orchestrated multi-step with lookup, and multiple consumed sources.
-
-```yaml
----
-naftiko: "0.5"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
- github_token: "GITHUB_TOKEN"
-info:
- label: "Project Dashboard"
- description: "Aggregates project data from Notion and GitHub into a unified API, with a pass-through proxy for direct access"
- tags:
- - dashboard
- - notion
- - github
- created: "2026-02-20"
- modified: "2026-02-20"
- stakeholders:
- - role: "owner"
- fullName: "Jane Doe"
- email: "jane.doe@example.com"
- - role: "editor"
- fullName: "John Smith"
- email: "john.smith@example.com"
-
-capability:
- exposes:
- - type: "api"
- port: 9090
- namespace: "dashboard"
- resources:
- # --- Forward proxy (simplest) ---
- - path: "/github/{path}"
- description: "Direct pass-through to the GitHub API for debugging"
- forward:
- targetNamespace: "github"
- trustedHeaders:
- - "Authorization"
-
- # --- Simple mode (direct call) ---
- - path: "/repos/{owner}/{repo}"
- description: "Retrieve a GitHub repository summary"
- name: "repo"
- inputParameters:
- - name: "owner"
- in: "path"
- type: "string"
- description: "Repository owner (user or organization)"
- - name: "repo"
- in: "path"
- type: "string"
- description: "Repository name"
- operations:
- - method: "GET"
- label: "Get Repository"
- call: "github.get-repo"
- with:
- owner: "$this.dashboard.owner"
- repo: "$this.dashboard.repo"
- outputParameters:
- - type: "string"
- mapping: "$.full_name"
- - type: "number"
- mapping: "$.stargazers_count"
- - type: "string"
- mapping: "$.language"
-
- # --- Orchestrated mode (multi-step call + lookup) ---
- - path: "/projects/{database_id}/contributors"
- description: "Lists project tasks from Notion and enriches each assignee with GitHub profile data"
- name: "contributors"
- inputParameters:
- - name: "database_id"
- in: "path"
- type: "string"
- description: "Notion database ID for the project tracker"
- operations:
- - name: "list-contributors"
- method: "GET"
- label: "List Project Contributors"
- steps:
- - type: "call"
- name: "query-tasks"
- call: "notion.query-database"
- with:
- database_id: "$this.dashboard.database_id"
- - type: "call"
- name: "list-github-users"
- call: "github.list-org-members"
- with:
- org: "naftiko"
- - type: "lookup"
- name: "match-contributors"
- index: "list-github-users"
- match: "login"
- lookupValue: "$.query-tasks.assignee"
- outputParameters:
- - "login"
- - "avatar_url"
- - "html_url"
- mappings:
- - targetName: "contributors"
- value: "$.match-contributors"
- outputParameters:
- - name: "contributors"
- type: "array"
- items:
- type: "object"
- properties:
- login:
- type: "string"
- avatar_url:
- type: "string"
- html_url:
- type: "string"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API for database and page operations"
- baseUri: "https://api.notion.com/v1"
- authentication:
- type: "bearer"
- token: "{{notion_token}}"
- inputParameters:
- - name: "Notion-Version"
- in: "header"
- value: "2022-06-28"
- resources:
- - name: "db-query"
- path: "/databases/{database_id}/query"
- label: "Database Query"
- operations:
- - name: "query-database"
- label: "Query Database"
- method: "POST"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "assignee"
- type: "string"
- value: "$.results[*].properties.Assignee.people[0].name"
- - name: "taskName"
- type: "string"
- value: "$.results[*].properties.Name.title[0].text.content"
-
- - type: "http"
- namespace: "github"
- description: "GitHub REST API for repository and user operations"
- baseUri: "https://api.github.com"
- authentication:
- type: "bearer"
- token: "{{github_token}}"
- resources:
- - name: "repos"
- path: "/repos/{owner}/{repo}"
- label: "Repositories"
- operations:
- - name: "get-repo"
- label: "Get Repository"
- method: "GET"
- inputParameters:
- - name: "owner"
- in: "path"
- - name: "repo"
- in: "path"
- outputParameters:
- - name: "full_name"
- type: "string"
- value: "$.full_name"
- - name: "stargazers_count"
- type: "number"
- value: "$.stargazers_count"
- - name: "language"
- type: "string"
- value: "$.language"
- - name: "org-members"
- path: "/orgs/{org}/members"
- label: "Organization Members"
- operations:
- - name: "list-org-members"
- label: "List Organization Members"
- method: "GET"
- inputParameters:
- - name: "org"
- in: "path"
- outputParameters:
- - name: "login"
- type: "string"
- value: "$[*].login"
- - name: "avatar_url"
- type: "string"
- value: "$[*].avatar_url"
- - name: "html_url"
- type: "string"
- value: "$[*].html_url"
-```
-
----
-
-### 4.6 MCP capability (tool exposition)
-
-Exposes a single MCP tool over Streamable HTTP that calls a consumed operation, making it discoverable by AI agents via the MCP protocol.
-
-```yaml
----
-naftiko: "0.5"
-externalRefs:
- - name: "env"
- type: "environment"
- keys:
- notion_token: "NOTION_TOKEN"
-info:
- label: "Notion MCP Tools"
- description: "Exposes Notion database retrieval as an MCP tool"
-
-capability:
- exposes:
- - type: "mcp"
- port: 3001
- namespace: "notion-tools"
- description: "Notion database tools for AI agents"
- tools:
- - name: "get-database"
- description: "Retrieve metadata about a Notion database by its ID"
- inputParameters:
- - name: "database_id"
- type: "string"
- description: "The unique identifier of the Notion database"
- call: "notion.get-database"
- with:
- database_id: "$this.notion-tools.database_id"
- outputParameters:
- - type: "string"
- mapping: "$.dbName"
-
- consumes:
- - type: "http"
- namespace: "notion"
- description: "Notion public API"
- baseUri: "https://api.notion.com/v1"
- authentication:
- type: "bearer"
- token: "{{notion_token}}"
- inputParameters:
- - name: "Notion-Version"
- in: "header"
- value: "2022-06-28"
- resources:
- - name: "databases"
- path: "/databases/{database_id}"
- operations:
- - name: "get-database"
- method: "GET"
- inputParameters:
- - name: "database_id"
- in: "path"
- outputParameters:
- - name: "dbName"
- type: "string"
- value: "$.title[0].text.content"
-```
-
----
-
-## 5. Versioning
-
-The Naftiko Specification uses semantic versioning. The `naftiko` field in the Naftiko Object specifies the exact version of the specification (e.g., `"0.5"`).
-
-Tools processing Naftiko documents MUST validate this field to ensure compatibility with the specification version they support.
-
----
-
-This specification defines how to describe modular, composable capabilities that consume multiple sources and expose unified interfaces, supporting orchestration, authentication, and flexible routing patterns.
\ No newline at end of file
diff --git a/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java
new file mode 100644
index 0000000..16160d2
--- /dev/null
+++ b/src/test/java/io/naftiko/engine/CapabilitySkillIntegrationTest.java
@@ -0,0 +1,216 @@
+/**
+ * Copyright 2025-2026 Naftiko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package io.naftiko.engine;
+
+import static org.junit.jupiter.api.Assertions.*;
+import java.io.File;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import io.naftiko.Capability;
+import io.naftiko.engine.exposes.ServerAdapter;
+import io.naftiko.engine.exposes.SkillServerAdapter;
+import io.naftiko.spec.NaftikoSpec;
+import io.naftiko.spec.exposes.SkillServerSpec;
+
+/**
+ * Integration tests for the Skill Server Adapter.
+ *
+ *
Verifies YAML deserialization, adapter wiring, lifecycle (start/stop), and live HTTP
+ * responses from the skill catalog endpoints.