1 Capability Spec
jfabian edited this page 2026-08-13 11:15:47 -03:00

VOX Capability Packaging & Wiring Specification

LEGACY — superseded. This spec predates the current capability implementation (see VOXCapability, Capability-Contract, and Capabilities-Reference). Concrete details here (e.g. ai.ollama, comm.telegram, cap_* attribute binding) describe an earlier design and are retained for history only.

Classification: Internal Architecture Specification
Scope: Capability module structure, declaration, configuration, and runtime binding
Compliance: Mandatory — all capability implementations must conform to this specification


Table of Contents

  1. Capability Directory Topologies
  2. Declarative Contract — capability.yml Schema
  3. Imperative Interface — capability.py Blueprint
  4. Wiring & Lifecycle Phase Sequence
  5. Configuration Parameter Resolution Order
  6. Compliance Verification

1. Capability Directory Topologies

Every capability resides under src/vox/capabilities/<category>/<name>/. The category is a single dot-separated segment (ai, comm, net). The name is a single segment matching the registered capability identifier.

Two topologies are permitted. The orchestrator's discovery mechanism (VOXOrchestrator._discover_capabilities) is topology-agnostic — it identifies a capability solely by the presence of capability.py at the directory root.

1.1 Monolithic Topology

All domain logic resides in capability.py. No internal modules.

src/vox/capabilities/
└── <category>/
    └── <name>/
        ├── __init__.py        # Re-exports capability class
        ├── capability.yml     # Declarative manifest (see §2)
        └── capability.py      # VOXCapability subclass + all domain logic

Constraints:

  • The __init__.py must export exactly one symbol: the VOXCapability subclass.
  • No additional .py files are permitted in this directory.
  • The capability.py file must not exceed 300 lines without explicit architectural review.

1.2 Modular Topology

Domain logic is decomposed into separate modules for client management, data modelling, and auxiliary processing. This is the recommended topology for any capability exceeding 150 lines of implementation logic.

src/vox/capabilities/
└── <category>/
    └── <name>/
        ├── __init__.py        # Re-exports capability class
        ├── capability.yml     # Declarative manifest (see §2)
        ├── capability.py      # VOXCapability subclass (entry point only)
        ├── client.py          # External API client wrapper
        └── models.py          # Dataclasses and type definitions

Optional modules (per domain need):

Module Responsibility
client.py Wraps all external I/O (HTTP, WebSocket, subprocess). Uses httpx.AsyncClient or equivalent. Implements aclose() if stateful.
models.py Immutable dataclasses for request/response types. No business logic.
sanitizer.py Input normalization, validation, and constraint enforcement.
cache.py Transient storage with TTL enforcement for reusable results.
router.py Conditional dispatch logic (e.g., local vs. cloud backend selection).
rag.py Retrieval-augmented generation logic operating on external file paths.

Constraints:

  • capability.yml and capability.py must exist at the directory root. Their absence causes the orchestrator to skip the directory during discovery.
  • The capability.py module is the sole entry point. It must not call capability.py-internal functions from external modules directly; all cross-module imports go through the capability's own package namespace.
  • Internal modules may import from each other freely via relative imports.

1.3 Topology Comparison

Property Monolithic Modular
File count 3 (minimum) 5+
Implementation logic location capability.py Distributed across client.py, models.py, plus optional modules
Orchestrator discovery boundary capability.py presence capability.py presence
Parameter hydration PARAMS dict only PARAMS dict only
Lifecycle hooks In capability.py class In capability.py class; delegates to sub-modules
Client pool location Inline in boot() Instantiated in boot(), managed in client.py
Recommended for Simple wrappers (<150 lines) Complex multi-operation drivers

2. Declarative Contract — capability.yml Schema

2.1 Schema Definition

# capability.yml — VOX Capability Declarative Manifest
#
# This file is read by the orchestrator during the manifest resolution phase.
# It must be valid YAML and conform to the schema defined below.

# --- Identity (required) ---
name:        <string>           # Fully qualified dotted name, e.g. "ai.ollama", "comm.telegram"
version:     <semver>           # Semantic version string, e.g. "1.0.0"

# --- Description (optional) ---
description: <string | block>   # Human-readable description of the capability's function

# --- Class Reference (optional, defaults to schema inference) ---
capability_class: <string>      # Named export from __init__.py; defaults to auto-detected VOXCapability subclass

# --- Interface Exposure (optional) ---
provides:
  - <string>                    # List of abstract interface identifiers this capability satisfies
                                # Used for interface-based capability resolution

# --- Parameters (optional) ---
params:
  <param_name>:                 # UPPER_CASE parameter key matching PARAMS dict key exactly
    description: <string>       # Human-readable description
    type: <type_reference>      # Target type: "string" | "int" | "float" | "bool" | "path" | "secret"
    default: <any> | null       # Default value if absent; null = REQUIRED at runtime
    sensitive: <bool>           # If true, value is masked in logs (default: false)

# --- Shorthand Parameter Declaration (alternative to `params`, for simple cases) ---
requires:
  - <param_name>                # List of REQUIRED parameter keys (defaults to null/None)
optional:
  <param_name>: <default_value> # Map of OPTIONAL parameter keys with their defaults

# --- Runtime Attributes (optional) ---
singleton: <bool>               # If true, only one instance is created globally (default: true)
stateless:  <bool>              # If true, no state is retained between invocations (default: true)

# --- Dependency Declarations (optional) ---
dependencies:
  system:                       # System-level package dependencies (checked at pre-flight)
    - <string>
  capabilities:                 # Capability-level dependencies (resolved at mount time)
    - <name>                    # Fully qualified capability names this capability depends on

2.2 Validation Rules

Rule ID Condition Action
V001 name does not match directory path (e.g. ai/ollama/ → ai.ollama) Orchestrator rejects manifest with validation error
V002 params.<key>.type is not one of the permitted type_reference values Orchestrator rejects manifest with validation error
V003 A key in requires or optional does not have a corresponding PARAMS dict entry Orchestrator issues a warning; runtime parameter hydration may fail
V004 dependencies.capabilities references a capability not found in the global registry Orchestrator issues a warning; mount order is non-deterministic
V005 version does not match the semver pattern <major>.<minor>.<patch> Orchestrator rejects manifest with validation error

2.3 Concrete Examples

Minimal manifest (OllamaCapability as currently deployed):

name: ai.ollama
version: 1.0.0
singleton: true
stateless: true

Full manifest (standardized form):

name: comm.telegram
version: 1.0.0

description: |
  Telegram communication bridge for VOX agents.
  Provides bidirectional messaging, long-polling, and media delivery.

capability_class: TelegramCapability

provides:
  - messaging
  - polling
  - notifications
  - voice_input

params:
  TELEGRAM_BOT_TOKEN:
    description: Telegram Bot API token
    type: secret
    default: null
    sensitive: true
  TELEGRAM_USER_ID:
    description: Authorized Telegram user identifier
    type: string
    default: null
  TELEGRAM_LONG_TIMEOUT:
    description: Long polling timeout in seconds
    type: int
    default: 20

singleton: true
stateless: false

comm.email manifest:

name: comm.email
version: 1.0.0

description: |
  Asynchronous email dispatch capability for VOX agents.
  Sends plain-text emails via SMTP using credentials from the
  bound agent's local .env file.

capability_class: EmailCapability

provides:
  - email

params:
  SMTP_HOST:
    description: SMTP server hostname
    type: string
    default: null
  SMTP_PORT:
    description: SMTP server port
    type: int
    default: 587
  SMTP_USER:
    description: SMTP authentication username
    type: string
    default: null
    sensitive: true
  SMTP_PASS:
    description: SMTP authentication password
    type: secret
    default: null
    sensitive: true

singleton: true
stateless: true

3. Imperative Interface — capability.py Blueprint

3.1 File Structure Mandate

Every capability.py must contain exactly one class inheriting from VOXCapability. This class is the runtime binding target.

[Mandatory] class <Name>Capability(VOXCapability):
    CAPABILITY_NAME = "<dotted.name>"     # matches manifest name
    PARAMS = { ... }                      # matches manifest params schema

3.2 Parameter Declaration Contract

The PARAMS dict uses the following structure:

PARAMS: dict[str, list[Any]] = {
    "PARAM_KEY": [description: str, default: Any | None],
}
  • The key ("PARAM_KEY") must match the key in capability.yml params or requires/optional sections exactly.
  • description is a human-readable string shown in explain_config() output.
  • default is the fallback value. If None, the parameter is considered REQUIRED and its absence during mount() raises a ValueError.

Validation rule: Every key in PARAMS with a None default must either be supplied by an agent-level configuration or declare a non-null fallback in capability.yml. Resolution order is defined in §5.

3.3 Lifecycle Hooks

class ExampleCapability(VOXCapability):
    """Every VOXCapability subclass must implement lifecycle hooks
    according to the following contract."""

    # ------------------------------------------------------------------
    # Pre-boot (class method, invoked during discovery)
    # ------------------------------------------------------------------

    @classmethod
    async def health_check(cls) -> bool:
        """Verify that the capability's runtime preconditions are met.

        Contract:
          - Return True if the capability can boot successfully.
          - Return False if a required system dependency is missing or
            an external service is unreachable.
          - Must complete within a 5-second timeout.
          - Should NOT allocate persistent resources (use boot() for that).

        Default implementation:
          return True
        """
        ...

    # ------------------------------------------------------------------
    # Boot (instance method, invoked during agent boot sequence)
    # ------------------------------------------------------------------

    async def boot(self) -> None:
        """Initialize all runtime resources required for operation.

        Contract:
          - All client connections, connection pools, and subprocesses
            must be allocated HERE, not lazily during method calls.
          - Must raise on failure — the orchestrator treats exceptions
            as a failed boot and marks the agent as ERROR.
          - Must complete within a configurable timeout (default: 30s).
          - After successful return, the capability is considered ready
            for invocation.
          - Must not depend on the state of any specific agent; boot()
            is called once per singleton instance, not per bound proxy.

        Default implementation:
          pass
        """
        ...

    # ------------------------------------------------------------------
    # Shutdown (instance method, invoked during agent teardown)
    # ------------------------------------------------------------------

    async def shutdown(self) -> None:
        """Release all runtime resources allocated during boot().

        Contract:
          - Must be idempotent — calling shutdown() multiple times
            must not produce errors.
          - Must not raise exceptions under normal operation.
          - All client connections, subprocesses, and background tasks
            must be terminated.
          - After return, the capability must not accept invocations.

        Default implementation:
          pass
        """
        ...

    # ------------------------------------------------------------------
    # Mount (instance method, invoked by the orchestrator during binding)
    # ------------------------------------------------------------------

    def mount(
        self,
        agent: "VOXAgent",
        config: dict[str, Any],
    ) -> "VOXBoundCapability":
        """Create a per-agent proxy with resolved configuration.

        Contract:
          - Validates config against PARAMS via validate_and_extract().
          - Returns a VOXBoundCapability wrapping self, the agent
            reference, and the sanitized parameter dict.
          - Raises ValueError if required parameters are missing.
          - Must not allocate resources — that is the responsibility
            of boot().

        Default implementation (inherited from VOXCapability):
          sanitized, missing = self.validate_and_extract(config)
          if missing:
              raise ValueError(f"Missing params for {self.name}: {missing}")
          return VOXBoundCapability(self, agent, sanitized)

        Override only if custom binding logic is required.
        """
        ...

3.4 Connection Pool Contract

# Mandatory Pattern — Single Boot-time Pool

class ExampleCapability(VOXCapability):

    _client: ExternalClient  # typed attribute, initialized in boot()

    async def boot(self) -> None:
        self._client = ExternalClient(
            base_url=self.PARAM_KEY_URL,      # hydrated from config
            timeout=float(self.PARAM_KEY_TIMEOUT),
        )

    async def shutdown(self) -> None:
        if hasattr(self, "_client"):
            await self._client.aclose()

    async def some_operation(self, ...) -> ...:
        return await self._client.some_method(...)

Constraints:

  • _client or equivalent must be typed (no bare self.client = ... without annotation).
  • boot() is the exclusive allocation site. Lazy initialization in operation methods is prohibited.
  • shutdown() must guard with hasattr() or equivalent to maintain idempotency.
  • The client class (e.g., OllamaClient, TelegramClient) must implement async def aclose(self) -> None for clean teardown.

3.5 Method Signature Guidelines

class ExampleCapability(VOXCapability):
    """Method signature rules for externally visible operations."""

    # Rule 1: Async methods only
    async def operation(self, ...) -> ResponseType:
        ...

    # Rule 2: Named parameters for optional configuration
    async def operation(
        self,
        required_param: str,
        *,
        optional_param: str | None = None,
        flag_param: bool = False,
    ) -> ResponseType:
        ...

    # Rule 3: Return typed responses, not raw dicts
    #   ✅ from .models import ResponseType
    #   ❌ return {"key": "value"}

    # Rule 4: Parameterized queries use the _build_options() pattern
    def _build_options(self) -> OptionsType:
        return OptionsType(
            field1=self.PARAM_KEY_1,
            field2=float(self.PARAM_KEY_2),
        )

3.6 Proxy-Mediated Access Semantics

When a method defined on VOXCapability is invoked through VOXBoundCapability, the proxy rewires self to point to the VOXBoundCapability instance, not the VOXCapability singleton. This provides the following access model:

Access Target Expression Resolved By
Configuration parameters self.PARAM_KEY VOXBoundCapability.__getattr__ → _params dict
Runtime attributes self._client VOXBoundCapability.__getattr__ → _instance_attrs dict
Logger methods self.log(...) VOXBoundCapability.log()
Event emission self.emit(event, ...) VOXBoundCapability.emit() → agent.emit()
Safe filesystem paths self.get_safe_path(sub, file) VOXBoundCapability.get_safe_path() → agent.get_safe_path()
Sibling capability access self.get_capability("ollama") VOXBoundCapability.get_capability() → agent.cap_ollama

Constraint: Capability methods must not access self._agent directly. The agent reference is available on the proxy but is not part of the capability's public contract. Direct agent access constitutes an ontological coupling violation (see Core-Ontology §1.3, constraint 1).


4. Wiring & Lifecycle Phase Sequence

4.1 Phase Diagram

┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 0 — DEPLOYMENT MANIFEST RESOLUTION                               │
│ Component: Orchestrator                                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  Orchestrator reads agent.yml                                           │
│    └── Extracts capabilities list for each agent                        │
│        e.g., agent.capabilities = ["ai.ollama", "comm.telegram", ...]    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 1 — CAPABILITY DISCOVERY                                         │
│ Component: Orchestrator._discover_capabilities()                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  for each dir in capabilities/ matching **/capability.py:               │
│    ├── Derive cap_id from relative path: "ai/ollama/" → "ai.ollama"    │
│    ├── Dynamically import capability.py via importlib                   │
│    ├── Find first VOXCapability subclass in module                     │
│    ├── Call cls.health_check()                                         │
│    ├── Create CapabilityEntry(cls=..., healthy=bool)                   │
│    └── Store in self.capability_registry[cap_id]                       │
│                                                                         │
│  capability.yml is NOT parsed during this phase.                       │
│  YAML parsing is reserved for Phase 4 (parameter hydration).          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 2 — AGENT BOOT & BINDING                                         │
│ Component: VOXAgent._mount_capabilities()                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  for cap_id in agent.capabilities:                                     │
│    │                                                                   │
│    ├── Orchestrator.get_capability_instance(cap_id)                    │
│    │     ├── Lookup in registry                                        │
│    │     ├── If no instance exists:                                    │
│    │     │     instance = cls()                                        │
│    │     │     instance.id = cap_id                                    │
│    │     │     instance.logger = orchestrator.logger                   │
│    │     └── Return singleton instance                                 │
│    │                                                                   │
│    ├── cap.mount(agent, agent.config)                                 │
│    │     ├── validate_and_extract(config) → sanitized dict             │
│    │     ├── VOXBoundCapability(self, agent, sanitized)                │
│    │     └── Return bound proxy                                        │
│    │                                                                   │
│    ├── Store as self.capabilities[short_name]                          │
│    ├── Set attribute: self.cap_{short_name} = bound_proxy              │
│    │                                                                   │
│    └── [if YAML declares dependencies.capabilities]:                   │
│          Verify each dependency exists in registry; warn if absent     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 3 — CAPABILITY BOOT (CONNECTION ALLOCATION)                      │
│ Component: VOXAgent.boot()                                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  for each mounted capability:                                          │
│    └── await cap.boot()                                                │
│          ├── Allocate connection pool / client                          │
│          ├── Start background tasks if any (polling loops, etc.)       │
│          ├── Report status via self.ok("...")                          │
│          └── Return None (or raise on failure)                         │
│                                                                         │
│  Agent state transitions: BOOTING → IDLE                               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 4 — RUNTIME INVOCATION                                           │
│ Component: Agent via VOXBoundCapability proxy                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  agent.cap_ollama.generate(system=..., prompt=...)                      │
│    │                                                                   │
│    └── VOXBoundCapability.__getattr__("generate")                      │
│          ├── Resolves to capability class method                        │
│          ├── Wraps in async wrapper (self → proxy)                     │
│          └── Calls method with proxy as self                           │
│                └── self.PARAM_KEY  → _params dict (hydrated config)    │
│                └── self._client    → _instance_attrs (boot alloc)      │
│                └── self.log(...)   → proxy logger                      │
│                                                                         │
│  Parameter resolution at invocation time:                              │
│    param_value = self._params.get("PARAM_KEY")                          │
│    # Returns agent-level override if present, else YAML default,       │
│    # else PARAMS default. See §5 for full resolution order.            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ PHASE 5 — SHUTDOWN (CONNECTION TEARDOWN)                               │
│ Component: Orchestrator.shutdown() → VOXAgent.shutdown()               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  for each mounted capability (reverse order):                          │
│    └── await cap.shutdown()                                            │
│          ├── Cancel background tasks                                    │
│          ├── Close client connections                                  │
│          ├── Release pool resources                                    │
│          └── Report status via self.log("...")                         │
│                                                                         │
│  Agent state transitions: ACTIVE/IDLE → SHUTTING_DOWN → DEAD          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4.2 State Machine

                    ┌──────────┐
                    │ DISCOVER │  (Phase 1 — class-level health_check)
                    └────┬─────┘
                         │ healthy=True
                    ┌────▼─────┐
                    │ REGISTER │  (stored in capability_registry)
                    └────┬─────┘
                         │ agent boot
                    ┌────▼─────┐
                    │  MOUNT   │  (Phase 2 — validate_and_extract → bound proxy)
                    └────┬─────┘
                         │
                    ┌────▼─────┐
                    │   BOOT   │  (Phase 3 — connection pool allocation)
                    └────┬─────┘
                         │ success
                    ┌────▼─────┐
                    │  ACTIVE  │  (Phase 4 — runtime invocations)
                    └────┬─────┘
                         │ shutdown
                    ┌────▼─────┐
                    │ SHUTDOWN │  (Phase 5 — client teardown)
                    └──────────┘

5. Configuration Parameter Resolution Order

5.1 Resolution Chain

When self.PARAM_KEY is accessed at runtime via the VOXBoundCapability proxy, the value is resolved through the following precedence chain (highest precedence first):

1. Agent-level override          agent.yml → capabilities → <cap_name> → params
   (supplied by per-agent configuration in the deployment manifest)

2. capability.yml default        manifest → optional → <param_name>
   (declared in the capability's own manifest file)

3. PARAMS dict default           capability.py → PARAMS → [key][1]
   (the default value hardcoded in the Python class definition)

4. None                          (fallback — raises ValueError if accessed)

5.2 Resolution Algorithm

def resolve_param(cap_id: str, param_key: str) -> Any:
    # Priority 1: agent-level override (from agent.yml per-capability config)
    agent_value = get_agent_override(cap_id, param_key)
    if agent_value is not None:
        return agent_value

    # Priority 2: capability.yml optional default
    yaml_default = get_yaml_default(cap_id, param_key)
    if yaml_default is not None:
        return yaml_default

    # Priority 3: PARAMS dict default
    python_default = get_python_default(cap_id, param_key)
    if python_default is not None:
        return python_default

    # Priority 4: None — required but missing
    return None  # caller must handle ValueError

5.3 Validation at Mount Time

VOXCapability.validate_and_extract() applies the resolution chain and collects all parameters whose final resolved value is None into a missing list. If missing is non-empty, mount() raises ValueError.


6. Compliance Verification

6.1 Automated Checks

The following conditions must be verifiable via static analysis or runtime test:

Check ID Scope Verification Method
C001 Structure capability.yml exists at directory root
C002 Structure capability.py exists at directory root
C003 Structure __init__.py exports exactly one VOXCapability subclass
C004 Structure capability.py contains exactly one VOXCapability subclass
C005 Contract PARAMS dict keys match capability.yml params/requires/optional keys
C006 Contract CAPABILITY_NAME matches capability.yml name field
C007 Lifecycle boot() calls super().boot() or equivalent initialization
C008 Lifecycle shutdown() is idempotent
C009 Pool _client or _pool attribute is typed and initialized in boot()
C010 Pool Client class implements async def aclose(self) -> None

6.2 Non-Compliance Resolution

Violation Severity Resolution
Missing capability.py BLOCKING Orchestrator skips directory; no capability registered
Missing PARAMS key referenced in YAML WARNING Runtime parameter hydration may produce unexpected defaults
health_check() returns False BLOCKING Capability is registered as unhealthy; agent boot fails
boot() raises BLOCKING Agent transitions to ERROR state
Proxy bypass (direct agent access) CRITICAL Architectural violation per Core-Ontology §1.3 constraint 1
Lazy client allocation WARNING Performance degradation on constrained VPS; review required
Non-idempotent shutdown() WARNING Risk of resource leaks during agent restart cycles

Appendix A: Terminology Reference

Term Definition
Capability Stateless mountable driver implementing a specialized operation. Zero identity, zero autonomy.
VOXCapability Base class defining the lifecycle hooks (boot, shutdown, mount, health_check) and parameter schema (PARAMS).
VOXBoundCapability Auto-generated proxy sealing the Agent↔Capability boundary. Provides parameter hydration, method delegation, and sandboxed access to agent facilities.
Manifest capability.yml — the declarative YAML schema describing the capability's identity, parameters, and dependencies.
Connection Pool A reusable set of client connections (HTTP, database, subprocess) allocated once during boot() and torn down during shutdown().
Parameter Hydration The runtime resolution of configuration values through the precedence chain: agent override → YAML default → PARAMS default → None.
Ontological Boundary The interface between Agent and Capability, enforced by VOXBoundCapability, through which only declared parameters may pass.

Appendix B: Directory Topology Quick Reference

src/vox/capabilities/
├── ai/                         # Artificial intelligence / LLM capabilities
│   ├── ollama/
│   │   ├── __init__.py
│   │   ├── capability.yml
│   │   ├── capability.py       # OllamaCapability(VOXCapability)
│   │   ├── client.py           # OllamaClient (httpx wrapper)
│   │   └── models.py           # OllamaChatRequest, OllamaMessage, etc.
│   └── ...
├── comm/                       # Communication capabilities
│   ├── email/
│   │   ├── __init__.py
│   │   ├── capability.yml
│   │   ├── capability.py       # EmailCapability(VOXCapability)
│   │   ├── client.py           # EmailClient (aiosmtplib wrapper)
│   │   └── models.py           # EmailConfig
│   ├── telegram/
│   │   ├── __init__.py
│   │   ├── capability.yml
│   │   ├── capability.py       # TelegramCapability(VOXCapability)
│   │   ├── client.py           # TelegramClient (httpx wrapper)
│   │   └── models.py           # TelegramConfig
│   ├── voicetotext/
│   │   ├── __init__.py
│   │   ├── capability.yml
│   │   ├── capability.py       # VoiceToTextCapability(VOXCapability)
│   │   ├── client.py           # WhisperClient (faster-whisper wrapper)
│   │   └── models.py           # WhisperConfig, TranscriptionResult
│   └── ...
└── net/                        # Network / browsing capabilities
    └── browser/
        ├── __init__.py
        ├── capability.yml
        ├── capability.py       # BrowserCapability(VOXCapability)
        ├── client.py           # BrowserClient (Playwright wrapper)
        └── models.py           # BrowserConfig