Skip to main content

Serving a capability

Start with a small operation that has no external effects. The public Skulk™ extension API lets a provider declare its input and output, join discovery, and handle calls inside a Skulk™ process.

Choose your integration

This guide uses skulk.extensions. A separately supervised plugin uses the managed capability SDK, with isolated dependencies and a different lifecycle. SDK 0.4.0 and child protocol 4 support unary and all three streaming modes. See managed streaming for that path and its release requirements.

unary

One request, one result

server_streaming

Request, progressive output

client_streaming

Progressive input, final result

bidirectional

Input and output together

1. Describe the operation​

The descriptor is the contract a caller discovers. Use an exact semantic version and JSON Schemas. Inputs are validated before your handler runs, outputs after it returns.

from skulk.extensions import CapabilityDescriptor

ECHO_DESCRIPTOR = CapabilityDescriptor(
id="echo", version="1.0.0", title="Echo",
description="Return the supplied text unchanged.",
io_mode="unary",
input_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"], "additionalProperties": False,
},
output_schema={
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
},
)

2. Implement the provider​

The extension has a name, a Skulk™ compatibility constraint, and a list of descriptors. A unary call returns a JSON object.

from skulk.extensions import CapabilityCall, ExtensionContext

class EchoProvider:
name = "echo-provider"
skulk_requires = ">=2.0,<3"

def chat_middleware(self) -> None:
return None

def capabilities(self) -> list[CapabilityDescriptor]:
return [ECHO_DESCRIPTOR]

async def handle_call(
self, context: ExtensionContext, call: CapabilityCall,
) -> dict[str, object]:
return {"text": call.payload["text"]}

3. Register the extension​

Add a Python entry point to your package. Install the package into a dedicated Skulk™ development environment and restart that node; entry points are discovered at startup.

[project.entry-points."skulk.extensions"]
echo-provider = "your_package:EchoProvider"

The complete reference echo package is available in the public Skulk™ repository. It includes the package layout and provider implementation. Its recorded example compatibility constraint is broader than the 2.0 constraint used here; test your own declared range.

4. Discover before calling​

The tag says where to look. The descriptor says what can be called. Fetch the exact descriptor and revision on each negotiation; never invent a revision or use an old cached descriptor as proof of readiness.

curl http://localhost:52415/v1/capabilities

The result has node_id, capabilities, and revisions. revisions maps an exact id@version to its content digest. A peer can be described with ?node_id=....

Use the descriptor explorer to inspect the schema, then call the pinned contract. Extension callers normally use context.describe_node and context.call_capability, rather than handling transport themselves.

5. Make failure part of the contract​

Test invalid input, output validation, deadline expiry, unavailable providers, and restart. A valid unary envelope returns HTTP 200 with ok and a typed result or error. Switch on error.code; malformed envelopes receive HTTP 422.

For all four I/O modes and the complete handler lifecycle, read the normative extension guide.