Table of Contents
- Capability Contract
- 0. Agent Contract (Cross-Reference)
- 1. The Mandatories
- 1.1 Inherit from VOXCapability
- 1.2 Declare CAPABILITY_NAME
- 1.3 Declare PARAMS
- 1.4 Declare SENSITIVE_PARAMS
- 1.5 Declare EXPOSED_COMMANDS (optional)
- 1.6 Implement Public Methods
- 1.7 Declare Role Dependencies via REQUIRES
- 1.8 NLU Guardrails via sample_prompts
- 2. The Lifecycle Hooks
- 3. What the Framework Guarantees
- 3.1 Parameter Resolution
- 3.2 Automatic self Injection
- 3.3 Freeze Protection
- 3.4 Vault Secret Injection
- 4. Contract Violations (What Must Never Happen)
- 5. Best Practices
- 6. Checklists
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Capability Contract
Every VOX capability must satisfy a minimal contract so the framework can load, configure, bind, and run it without crashing. This document defines that contract and the guarantees the framework makes in return.
0. Agent Contract (Cross-Reference)
This document covers the capability contract. Every capability is consumed
by an agent. For the agent-side contract — including agent.yml schema,
role loading, and lifecycle — see VOXAgent and VOXRole.
Key points:
agent.ymlmust containname(str) andid(str). Wrong types are rejected at boot; unknown fields are warned.- Roles declare capability dependencies via the
REQUIRESclass variable (see §1.5 below), discovered by static AST analysis. - Missing capabilities degrade, not crash — a role whose required
capability is unavailable is disabled and the agent is flagged
DEGRADED.
1. The Mandatories
1.1 Inherit from VOXCapability
from vox.capabilities.base import VOXCapability
class MyCapability(VOXCapability):
...
All lifecycle, parameter extraction, and mount logic live on the base class. Skipping it breaks the entire binding mechanism.
1.2 Declare CAPABILITY_NAME
A globally unique dotted identifier used for lookup and role declarations.
CAPABILITY_NAME = "ai.llm"
Roles request capabilities via REQUIRES:
REQUIRES = {"ai.llm", "comm.gateway"}
After mounting, the agent stores the bound proxy in self.agent.capabilities[ "ai.llm"] — not as a cap_llm attribute.
1.3 Declare PARAMS
Every configuration parameter the capability needs must be listed so mount()
can validate and extract it before the bound proxy is created.
PARAMS = {
"API_URL": ["Service endpoint URL", "http://localhost:8080"],
"API_KEY": ["Authentication token", None], # None = REQUIRED
"TIMEOUT": ["Request timeout (seconds)", 30.0],
}
Format: {param_name: [human_description, default_or_None]}.
- A
Nonedefault makes the parameter required — the agent is flaggedDEGRADEDif it is missing at mount time. - An empty string
""default means unconfigured / feature inactive. - Optional credentials MUST default to
"", neverNone— aNonedefault on a system capability mounted on every agent (e.g.comm.gateway) degrades every agent that does not use the feature. See Capabilities Reference.
Mutable class-level PARAMS dicts are an intentional pattern — keep
# noqa: RUF012 on them.
1.4 Declare SENSITIVE_PARAMS
Secrets are injected from the per-agent vault, never from source or logs.
SENSITIVE_PARAMS = {"API_KEY"}
At boot, CapabilityBinder.inject_vault_secrets() replaces each sensitive
param with its vault-stored value. If a sensitive param cannot be resolved and
the vault is unavailable (VOX_MASTER_KEY not set), boot fails fast with
VaultAccessError. When the vault exists but a value is missing, the
capability's roles are disabled and the agent is flagged DEGRADED.
1.5 Declare EXPOSED_COMMANDS (optional)
Capabilities can expose commands callable through the agent's command registry:
EXPOSED_COMMANDS = [
{"name": "send_email", "description": "Send an automated plain-text email.", "method": "send_email"},
]
Each exposed command is registered on the agent and routed through the capability's bound method.
1.6 Implement Public Methods
Agents call capability methods on the bound proxy:
result = await self.agent.capabilities["ai.llm"].generate(system, prompt)
All public methods must:
- Be
async— the event loop must never block. - Accept
selfas the bound proxy instance (the__getattr__wrapper injects it automatically). - Accept keyword arguments rather than opaque positional blobs.
- Return a serializable value (
str,dict,list,None, etc.).
No generic run("command") dispatch. Each capability exposes its own named
methods with explicit signatures.
1.7 Declare Role Dependencies via REQUIRES
Every role must declare which capabilities it needs via the REQUIRES class
variable. The ASTAgentAnalyzer scans role files statically (top-level
REQUIRES = {...} sets) so capabilities are mounted before roles run.
from vox.roles import VOXRole, command
class MyRole(VOXRole):
REQUIRES = {"ai.llm", "comm.gateway"} # <-- capability IDs
@command("do_thing", description="Does a thing")
async def do_thing(self, ...):
...
Rules:
REQUIRESis aset[str]of fully-qualified capability IDs.- A role that needs no capabilities leaves
REQUIRES = set()(the default). - If a required capability is missing, the role is disabled and the agent
is flagged
DEGRADED(no crash).
1.8 NLU Guardrails via sample_prompts
Every @command can include sample_prompts — example phrases that help the
NLU classifier distinguish user intent:
from vox.roles import VOXRole, command
class MyRole(VOXRole):
REQUIRES = {"ai.llm"}
@command(
"search_products",
description="Search for products in the catalog",
sample_prompts=[
"search for blue shoes",
"find me a laptop under 1000",
"show me red dresses",
],
)
async def search_products(self, ...):
...
The NLU prompt rendered to the LLM becomes:
Available commands:
- search_products: Search for products in the catalog
e.g. "search for blue shoes"
e.g. "find me a laptop under 1000"
e.g. "show me red dresses"
- general_chat: casual conversation, no specific command.
e.g. "hello"
e.g. "how are you"
These few-shot examples are the strongest tool for guiding LLM classification. Without them, the LLM must infer intent from the command name alone.
The chat role also defines counter-examples for the general_chat fallback —
phrases that look like commands but should always be treated as conversation.
Rules:
- Prompts should match the language the agent actually processes.
- Provide 3–5 diverse examples per command.
- Use
general_chatexamples to cover common conversational openers. - The
CommandInfodataclass stores these; they are rendered automatically by the chat role's NLU prompt builder.
2. The Lifecycle Hooks
The framework calls these at specific points. All are optional — the base class provides safe no-op defaults.
| Method | When | Purpose |
|---|---|---|
health_check() |
At discovery, orchestrator startup | Verify the external service is reachable; return True/False |
boot() |
After mount, before first use | Start background tasks, open connections, begin polling loops |
shutdown() |
On agent teardown | Close connections, cancel tasks, release resources |
health_check (classmethod)
@classmethod
async def health_check(cls) -> bool:
...
Called at discovery. Unhealthy capabilities are flagged but still registered.
boot()
async def boot(self) -> None:
self._client = await create_client(...)
self._poll_task = asyncio.create_task(self._poll_loop())
Called after the bound proxy is created (see §3.2). At this point
self.PARAM_NAME works because self is the bound proxy. Use it to initialize
state that needs configuration values.
shutdown()
async def shutdown(self) -> None:
self._poll_task.cancel()
await self._client.close()
Called during agent teardown. Must be idempotent — the framework may call it more than once in error paths.
3. What the Framework Guarantees
3.1 Parameter Resolution
When mount() runs, it:
- Reads the agent's config dict.
- Extracts every param listed in
PARAMS. - Fills missing optional params with their declared defaults.
- Leaves required params (
Nonedefault) unresolved if absent. - Stores the resolved dict on the bound proxy.
Missing required params are reported by bound.validate_params(); the
CapabilityBinder flags the agent DEGRADED. bound.initialize() raises
ValueError listing the missing params.
3.2 Automatic self Injection
The VOXBoundCapability.__getattr__ wrapper ensures that when an agent calls:
await self.agent.capabilities["ai.llm"].generate(system, prompt)
It is rewritten to:
await LLMCapability.generate(bound_proxy, system, prompt)
So the method's self is the bound proxy, not the stateless capability
class. This gives your method access to:
self.PARAM_NAME— resolved configuration valuesself.log()/self.warning()/self.error()/self.ok()— loggingself._agent— the agent that owns this capabilityself._capability— the stateless capability singletonself.get_capability(cap_id)— sibling capability lookupself.get_safe_path(sub_dir, filename)— sandboxed asset path
3.3 Freeze Protection
After mounting, bound.freeze() makes the proxy immutable — any attempt to set
a public attribute raises AttributeError. Private _-prefixed attributes and
_instance_attrs remain mutable for storing runtime state.
3.4 Vault Secret Injection
Sensitive params are populated from the agent's encrypted vault at boot. Never
fall back to embedding secrets in PARAMS defaults, source, tests, or logs.
4. Contract Violations (What Must Never Happen)
| Violation | Consequence |
|---|---|
Blocking the event loop (time.sleep(), sync HTTP, etc.) |
Freezes the entire agent. Use asyncio.sleep(), aiohttp/httpx.AsyncClient, etc. |
| Raising unhandled exceptions in public methods | Propagates to the agent, may crash the role. Catch and log instead. |
| Accessing the filesystem, network, or OS without going through a capability | Breaks the OCAP security model. All I/O must be capability-gated. |
| Mutating the bound proxy's params at runtime | The proxy is frozen. Use _instance_attrs (private) for mutable state. |
Holding a reference to the raw VOXCapability singleton and calling methods on it directly |
Bypasses param injection. Always call through the bound proxy (self inside a method, or self.agent.capabilities[cap_id] outside). |
Defaulting optional credentials to None |
Degrades agents that do not use the feature (config sentinel violation). Default to "". |
| Logging or committing secret values | Breaks the vault model. Secrets live in the vault only. |
5. Best Practices
- Log everything. Use
self.log(message),self.warning(message),self.error(message),self.ok(message)— they prefix the capability name automatically. - Design for idempotency.
shutdown()may be called multiple times.generate()should be safe to retry. - Keep methods focused. A capability is a gateway — one method per operation.
- Use type hints. The contract doesn't enforce them, but they enable static analysis in agents.
- Test
health_check()first. If it fails, the capability won't be healthy at discovery.
6. Checklists
For a New Capability
- Subclass
VOXCapability - Set
CAPABILITY_NAME(dotted, unique) - Declare all params in
PARAMS(required params haveNonedefault; optional credentials default to"") - Declare secrets in
SENSITIVE_PARAMS(vault-injected) - Implement
asyncpublic methods with explicit signatures - Optionally declare
EXPOSED_COMMANDS - Optionally override
health_check(),boot(),shutdown() - Create
capability.ymlalongsidecapability.py - Never block
- Never mutate the proxy
- Never raise unhandled exceptions from public methods
For a New Role
- Subclass
VOXRole - Set
REQUIRESto the set of capability IDs the role needs - Decorate command handlers with
@command(name, description) - Register event handlers with
self.on(event_name) - Use
self.agent.capabilities["comm.gateway"].send_text(...)for outbound messages (or emitsend_messageto the chat role) - Guard optional capability access with
self.agent.capabilities.get("cap_id")
Overview
- Home
- Versioning
- Architecture Overview
- Building Blocks
Agent Model
- VOXAgent
- Agent Lifecycle
- VOXRole
- Creating an Agent
Capabilities
- VOXCapability
- Capabilities Reference
- Capability Contract
- Comm Gateway
- WhatsApp Setup
Messaging
- VOXMessage
- Messaging Contract
- Hierarchical Messaging Contract
- The War Room
Orchestrator & Control Plane
- VOXOrchestrator
- Orchestrator & Control Plane
- VOXApiServer
Persistence & Forensics
- Segmented Persistence & Forensic Traceability
Security & Governance
- Security Architecture
- Core Ontology (legacy)
- Capability Specification (legacy)
Status: v0.5.4