Framework API Overview¶
The framework is the extensible core of Consortium. Everything an operator interacts
with at runtime (listeners, agents, background plugins, and event hooks) is built from
components: self-contained projects that live under consortium/components/ and are
discovered and loaded when the server starts.
This page covers the concepts that are shared across every component type: how a component project is laid out, how components are identified (the difference between labels, names, and IDs), and how dependencies are declared and resolved. For more information on each type of component, check the following pages:
Scaffold a component instead of writing it by hand
The Create action of the
component manager (manage_components.py)
generates a ready-to-edit project for any of the four component types, with the
directory, manifest.json, and commented reference stubs already in place. The
concepts below still apply, and understanding them makes the generated files much
easier to work with.
The four component types¶
| Type | Directory | Entry point | What it does |
|---|---|---|---|
| Listener | components/listeners/ |
listener_template:ListenerTemplate |
Runs a network server that agents connect through |
| Agent | components/agents/ |
agent_template:AgentTemplate |
Builds agent payloads and handles their tasks and results |
| Plugin | components/plugins/ |
plugin:Plugin |
Runs a persistent background loop alongside the server |
| Event hook | components/event_hooks/ |
event_hook:EventHook |
Reacts to discrete framework events as they fire |
Listeners and agents are profiles: several cooperating classes spread across a few files. Plugins and event hooks are a single class in a single file. Regardless of type, every component follows the same discovery and identity rules described below.
Component project structure¶
Each component is a directory under its type's folder. Every component directory contains
a manifest.json, plus the module file(s) that hold the entry point class:
manifest.json is what the loader looks for when it walks the components tree. It points
at the entry point class and declares whether the component should be loaded:
entry_pointis<module>:<Class>, where<module>is the module file (without the.py) relative to the component directory. Each type has a conventional entry point class name, listed in the table above.enabledcontrols whether the loader picks the component up. Set it tofalseto leave a component in place but unloaded.
The loader recursively searches each type directory for manifest.json files, so a
component folder can sit at any depth (for example
components/agents/consortium/http/), not just directly under the type folder.
Identity: labels, names, and IDs¶
Every component carries three distinct kinds of identifier, and they are easy to confuse. They exist for different reasons and are set at different times.
| Identifier | Example | Set by | Stable? | Purpose |
|---|---|---|---|---|
| Label | consortium.plugins.agent_report |
Component author | Yes (fixed for the component) | Globally unique reference used for persistence and dependencies |
| Name | Agent Report Plugin |
Author / operator | No (freely editable) | Human-readable display text |
| ID | f47ac10b-... (uuid.UUID) |
Framework | No (new one every run) | Identifies one running instance for the duration of the server |
Label¶
The label is the component's stable, globally unique identifier. It is a class
attribute the author declares, and it is validated at class-definition time (a missing or
empty label fails at import, not at runtime). Because it never changes, the framework
uses it wherever a component must be referenced durably:
- In persisted configuration and across server restarts.
- As the key that other components target when they declare a dependency (see Component dependencies).
Because labels must be unique and stable across an entire installation, they follow a reverse-DNS naming convention:
consortium.plugins.agent_report
consortium.listeners.tcp_json
consortium.event_hooks.agent_activity_tracker
acme.agents.recon_agent
<namespace>is a vendor or author namespace (the built-in components useconsortium). Pick your own for components you publish, so your labels never collide with anyone else's.<component-type>is one oflisteners,agents,plugins, orevent_hooks.<component-name>is asnake_casename for the specific component.
The label is deliberately not derived from the directory path or the file name, so you can move or rename a component's folder without breaking references to it. The label is the contract; the folder layout is just where the code happens to live.
Name¶
The name is the human-readable display text shown in the client and logs. It is
free-form and can be changed at any time without consequence, because nothing references
a component by its name.
- For plugins and event hooks,
nameis a class attribute the author sets. - Listener and agent profiles also have a class-level
name, but that is the name of the profile. Each running listener or agent has its own separate instance name, supplied when it is created or registered and editable by the operator. A single agent profile can therefore produce many differently named running agents.
If a component omits name, the framework falls back to using the label as the display
name.
ID¶
An ID identifies a single running instance, not the component definition. Each running
plugin, listener, agent, or event hook is assigned a uuid.UUID (self.plugin_id,
self.listener_id, self.agent_id, self.event_hook_id) when it is instantiated. IDs
are generated fresh every time and are not stable across restarts, so they are only
useful for addressing a live instance during the current server session, never for
durable references.
Label vs. name vs. ID at a glance¶
- Use the label when you need to refer to a component definition durably (config, dependencies). One label, fixed forever.
- Use the name for anything a human reads. Cosmetic, editable, non-unique.
- Use the ID to address one live instance right now. Ephemeral, framework-assigned.
Type names are a different thing
ListenerType.name and AgentType.name (for example tcp_json or recon_agent)
are not component labels. They name a transport or agent family and are used to
match agents to the listeners they may connect through (an agent template lists
compatible_listener_types = {"tcp_json"}). A type name identifies a family across
profiles; a label identifies a single component project. See
ListenerType and
AgentType.
Shared metadata¶
Alongside label and name, every component entry class declares the same core
metadata. It is all validated at class-definition time, so malformed values fail at
import rather than surfacing later at runtime.
| Attribute | Type | Default | Description |
|---|---|---|---|
label |
str |
(required) | Stable reverse-DNS identifier (see above) |
name |
str |
label |
Human-readable display name |
description |
str |
"" |
Short description of what the component does |
version |
str |
None |
The component's own version, a PEP 440 version |
compatible_framework_version |
str |
None |
PEP 440 specifier for the framework versions this component supports |
authors |
set[str] |
set() |
Author names |
version is the component's own version string (for example "0.1.0"), parsed as a
PEP 440 version. It is what other components match
against when they depend on this one.
compatible_framework_version is a
PEP 440 version specifier (for example ">=0.1.0").
At load time the framework compares the running framework version against this specifier
and refuses to load a component that does not cover it. Leave it unset for no constraint,
or pin it when the component relies on a specific framework API.
Dependency management¶
A component can depend on two very different things, and the framework treats them separately:
- Third-party Python packages from PyPI (declared in
pyproject.toml). - Other Consortium components (declared as
component_dependencieson the class).
Third-party dependencies (PyPI packages)¶
If a component imports a package that is not part of the standard library or the
framework, declare it in a pyproject.toml in the component project's root directory,
next to manifest.json:
At load time the framework reads [project].dependencies and, for each entry, checks
that the package is already importable in the server's environment and that its
installed version satisfies the specifier. If a dependency is missing or the wrong
version, the component is not loaded and the framework reports why.
The framework does not install packages for you
Declaring a dependency in pyproject.toml is a check, not an install step. The
framework verifies the package is present; it never fetches anything. You must install
third-party dependencies into the same environment the server runs in yourself. For
the standard uv-managed setup, where each component is a workspace package, that
means uv add <package> from within the component's package (then
uv sync --all-packages), which matches how the rest of the project manages
dependencies.
For a component that is a uv workspace package, the usual workflow is:
- From the component project's root directory, run
uv add <package>. - Let
uvupdate that component'spyproject.tomland the workspace lockfile. - Run
uv sync --all-packagesso the server environment contains the dependency.
The framework's load-time check then verifies the installed package and its version; it does not replace either of the installation steps above.
Component dependencies (on other components)¶
A component can also require that another Consortium component is present and loaded
first. These are declared as component_dependencies, a set of
PEP 508 requirement strings whose names are the
labels of the components you depend on:
class Plugin(BasePlugin):
label = "consortium.plugins.agent_report"
...
component_dependencies = {
"consortium.plugins.persistent_listeners>=0.1.0",
}
Because a label is stable and unique, it is a safe thing to depend on. At load time the framework:
- Confirms every named dependency label is registered, and that its
versionsatisfies the specifier (missing or incompatible dependencies cause the dependent component to be skipped, not the whole load to fail). - Builds a dependency graph from the surviving components and topologically sorts it, so a component is always started after everything it depends on.
- Aborts the load if the graph contains a cycle.
Where to go next¶
- Build one end to end: Listeners, Agents, Plugins, Event Hooks.
- Skip the boilerplate with the Create action of the component manager.