Writing a Capability¶
A capability is a YAML document describing what your service exposes (REST routes, MCP tools, skill groups, management port) and what it consumes (upstream HTTP APIs, secrets). The ikanos engine turns that spec into a running server — no code required.
Inline Spec¶
The simplest approach — the full ikanos spec lives directly inside the
Capability CR under spec:
apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
name: hello-world
namespace: default
labels:
naftiko.io/tier: standard
spec:
ikanos: "1.0.0-alpha3"
info:
display: Hello World
description: "Simple REST capability"
labels:
naftiko.io/tier: standard
capability:
exposes:
- type: rest
port: 3001
namespace: my-api
resources: # alpha3 — keyed map
hello:
path: /hello
operations: # alpha3 — keyed map
get-hello:
method: GET
outputParameters:
- name: message
type: string
value: "Hello, World!"
When to use: quick tests and prototypes. Not recommended for production because updating the spec requires patching the CR directly.
specRef Pattern (Recommended)¶
The ikanos spec lives in a ConfigMap. The Capability CR references it via
specRef. This decouples the spec lifecycle from the CR lifecycle — you can
update the spec without touching the CR.
apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
name: hello-world
namespace: default
labels:
naftiko.io/tier: standard
spec:
specRef:
configMap: hello-world-spec # ConfigMap in the same namespace
key: capability.yaml # default key — can be omitted
Create the ConfigMap from your spec file:
kubectl create configmap hello-world-spec \
--from-file=capability.yaml=hello-world.yaml \
-n default
Update the spec without touching the CR:
kubectl create configmap hello-world-spec \
--from-file=capability.yaml=hello-world.yaml \
-n default --dry-run=client -o yaml | kubectl apply -f -
# Force reconcile
kubectl annotate capability hello-world \
reconcile-at=$(date +%s) --overwrite -n default
The operator reads the raw YAML verbatim — no re-serialization through the Java model. Every field you write, including MCP tools, aggregates, prompts, and custom attributes, is preserved exactly.
Expose Types¶
| Type | Purpose | Port |
|---|---|---|
rest |
HTTP REST API | your choice |
mcp |
MCP tool server (AI agents) | your choice |
skill |
Skill discovery endpoint | your choice |
control |
Observability — /metrics, /health/* |
typically 9090 |
Every expose entry gets its own named Service port. The control port also
triggers a ServiceMonitor and Prometheus pod annotations.
Multi-Port Capabilities¶
A capability can expose multiple adapters on different ports simultaneously. Skipper creates one named Service port per expose entry.
capability:
exposes:
- type: mcp
address: "0.0.0.0"
port: 3001
namespace: shipyard-tools
authentication:
type: bearer
token: "{{MCP_SERVER_TOKEN}}"
tools:
- name: list-ships
# ...
- type: rest
address: "0.0.0.0"
port: 3002
namespace: shipyard-api
resources:
- path: /ships
# ...
- type: skill
address: "0.0.0.0"
port: 3003
namespace: shipyard-skills
skills:
- name: fleet-ops
# ...
- type: control
address: "0.0.0.0"
port: 9090
observability:
enabled: true
metrics:
local:
enabled: true
traces:
sampling: 1.0
propagation: w3c
The resulting Service:
Note: always set
address: "0.0.0.0"on every expose when running in Kubernetes. Without it, the engine binds tolocalhostand the port is only reachable from inside the container.
Selecting a CapabilityClass¶
The naftiko.io/tier label drives resource allocation:
| Tier | CPU req/limit | Memory req/limit | HPA min/max |
|---|---|---|---|
standard |
250m / 500m | 256Mi / 512Mi | 1 / 4 |
premium |
500m / 1000m | 512Mi / 1Gi | 2 / 20 |
dev |
50m / 100m | 64Mi / 128Mi | 1 / 2 |
If no label is set or no matching class is found, standard defaults apply.
Tier Metadata Labels¶
Beyond tier, you can attach additional metadata labels for cost attribution
and discoverability:
metadata:
labels:
naftiko.io/tier: standard
naftiko.io/domain: platform
naftiko.io/cost-center: team-x
These labels propagate to the generated Deployment, Service, and pods.
Ingress (Public Exposure)¶
To expose a capability outside the cluster, add the public tag:
exposes:
- type: rest
port: 3001
namespace: my-api
tags: [public] # triggers Ingress creation
resources:
# ...
Skipper creates an Ingress targeting the tagged expose port.
Only business ports can be tagged public — the control port is never exposed
via Ingress.
Full Spec Reference¶
apiVersion: naftiko.io/v1alpha3
kind: Capability
metadata:
name: my-capability
namespace: default
labels:
naftiko.io/tier: standard # required — selects CapabilityClass
naftiko.io/domain: platform # optional metadata
naftiko.io/cost-center: team-x # optional metadata
spec:
# ── Option A: specRef (recommended) ──────────────────────────────────────
specRef:
configMap: my-capability-spec # ConfigMap in the same namespace
key: capability.yaml # default — can be omitted
# ── Option B: inline spec ─────────────────────────────────────────────────
ikanos: "1.0.0-alpha3"
info:
display: "My Capability"
description: "What this capability does"
tags: [rest, internal]
labels:
naftiko.io/tier: standard
naftiko.io/domain: platform
binds:
- namespace: "my-secrets"
location: "file:///./shared/secrets.yaml"
keys:
API_TOKEN: "my-api-token"
capability:
consumes:
- namespace: upstream
type: http
baseUri: "https://api.example.com"
resources: [ ... ]
exposes:
- type: rest
address: "0.0.0.0"
port: 3001
namespace: my-api
tags: [public]
resources: [ ... ]
- type: control
address: "0.0.0.0"
port: 9090
observability:
enabled: true
metrics:
local:
enabled: true
traces:
sampling: 1.0
propagation: w3c
status: # written by the operator — read-only
phase: Running | Failed
endpoint: http://my-capability.default.svc.cluster.local:3001
conditions:
- type: Ready
status: "True"
Reconciliation Triggers¶
The operator reconciles a Capability when:
- The CR is created or updated
- A child resource (ConfigMap, Deployment, Service) is modified or deleted
- The CR is annotated with
reconcile-at: