Data-First Capabilities¶
The Op/capability-declaration split — executable Ops registered by stable ID, inert capability declarations that reference them, and the loader + load-time validator that resolves declarations against the Op registry.
Details¶
What¶
A capability has two halves that work-buddy keeps separate:
- An Op — the executable callable. Python code, registered under a stable
op.<namespace>.<name>ID in the Op registry (work_buddy/mcp_server/op_registry.py). Built-in ops use theop.wb.*namespace and are organized by category underwork_buddy/mcp_server/ops/— one module per category, registering its ops as an import side effect. - A capability declaration — inert data. A
kind: "capability"knowledge-store unit carrying prose (name, description, aliases), the parameter schema, runtime metadata (mutates_state,retry_policy,requires,consent_operations,invokes,is_action,intrinsic_amplifiers,param_aliases,auto_retry,slash_command), and anopfield naming the Op it wraps.
This mirrors how workflows work: a workflow is inert data that references capabilities by name; a capability declaration is inert data that references an Op by ID. Executable code (Ops) is held apart from the editable, shareable, agent-authorable data (declarations).
Registration¶
Every capability is a declaration. The capability loader (work_buddy/knowledge/capability_loader.py) reads each kind: "capability" unit, resolves its op against the Op registry, checks the schema_version is recognized (wb-capability/v1), and validates the declared parameter schema against the resolved callable's signature. Resolved capabilities are merged into the gateway registry by _build_registry() before the tool-requirements filter pass, so declared capabilities with unmet requires are filtered by the same logic as any other.
Op registry¶
work_buddy/mcp_server/op_registry.py is an in-memory table keyed by op ID:
register_op(op_id, fn)— register a callable; validates theop.<namespace>.<name>grammar and rejects duplicate IDs.get_op(op_id)/list_ops()— lookup.load_builtin_ops()— imports thework_buddy/mcp_server/ops/package, whose modules register their ops as an import side effect.register_op_effects(op_id, effects)/get_op_effects(op_id)— for capabilities with multi-effect manifests. AnEffectSpecholds aresolvercallable, so it cannot ride in a data declaration; the ops module registers the manifest and the loader threads it onto the resolvedCapability.
The registry keeps no state across a reload: mcp_registry_reload purges work_buddy.* from sys.modules, so the table rebuilds fresh on the next registry build.
Load-time validation¶
Resolution failures (missing op, unknown schema version, signature mismatch) are reported by the capability_op_resolution check in docs_validate as warnings — an unresolved declaration is surfaced but does not block the store. The live-store invariants in tests/unit/test_capability_declarations_invariant.py promote these to hard CI errors: every capability unit must be a declaration, every op id must be registered and well-formed, every declaration must resolve with zero issues, and the resolved count must equal the unit count (no silent drops).
Why the split exists¶
Welding prose to Python source blocks three things: editing a capability's description through gateway tools, shipping capabilities as inert shareable artifacts, and letting an agent synthesize a capability without writing Python. A declaration has none of those limits — its prose is editable via docs_update, it is inert data safe to share, and an agent can author one. This is the executable/inert seam recorded as commitment C1 in the ecosystem architecture.