Extensions And Capabilities¶
Capabilities and extensions are independent dimensions in LoopX:
- a capability describes what LoopX can do and the product contract exposed to callers;
- an extension is a delivery unit that can provide one or more capabilities and has its own installation, enablement, disablement, and upgrade lifecycle.
Built-in capabilities and extension-provided capabilities share one registry.
Implementation directories do not become capabilities merely because they live
under loopx/capabilities/; registration is explicit.
LoopX Core
|-- capability contracts
|-- built-in capability registrations
`-- extension runtime
|-- extension A -- provides a new capability
|-- extension B -- implements a core capability
`-- extension C -- remains disabled
Runtime Responsibilities¶
Capability and extension are code and delivery boundaries. At runtime, keep four responsibilities distinct:
| Responsibility | Contract |
|---|---|
| Agent | Plans and performs one bounded action through a host/runtime and an available capability. |
| Provider | Calls an external system and returns a bounded observation, effect result, or readback. |
| Capability | Normalizes provider output, applies domain policy and validation, and proposes a finite transition. |
| LoopX Kernel | Accepts or rejects that proposal and owns durable todo, gate, monitor, writeback, quota, recovery, and scheduling state. |
The normal flow is:
Agent -> Capability -> Provider -> external system
Provider readback -> Capability transition proposal -> LoopX Kernel
Provider is an implementation role. When it implements a LoopX capability,
it is registered under that capability; a standalone extension provider may
instead expose only its own bounded command. A provider may be built into LoopX
or delivered by an extension. Extension is not a fifth runtime role: it owns
provider packaging, installation, enablement, upgrade, and compatibility
lifecycle. It does not own domain transition policy or goal state. Domain state
and receipts are data crossing these boundaries, not independent actors.
Repository Layout¶
The repository uses one path for each ownership boundary:
loopx/capabilities/<capability>/ caller-facing contracts and core providers
loopx/extensions/ extension lifecycle and bundled providers
packages/<package-id>/ independently installable distributions
loopx/extensions/ is a Python package shipped in the LoopX wheel.
packages/ is a monorepo package root; its children have their own packaging
metadata and do not become part of the LoopX wheel. There is intentionally no
repository-root extensions/ directory. That duplicate name obscured whether a
path represented importable LoopX code or a separately installable artifact.
The layout does not merge capabilities and extensions. They remain separate axes and compose through the capability/provider registry: a capability names the stable outcome contract, while an extension owns provider delivery and lifecycle.
Registration Model¶
Every registered capability declares three provider-facing fields:
origin:builtinorextension;visibility:publicorinternal;provider_id:loopx-coreor the extension manifest id.
The built-in catalog remains the default source. Extension manifests declare providers and contracts; the extension runtime state is the only source for whether each provider is installed, enabled, and doctor-ready. Duplicate capability or provider ids fail closed. Internal registrations remain available to the registry but are omitted from the public catalog.
Catalog discovery does not scan arbitrary directories or import extension Python code. A caller can add a declaration-only manifest to a catalog read:
loopx capability list \
--extension-manifest /path/to/extension.toml \
--format json
loopx capability show lark-kanban \
--extension-manifest /path/to/extension.toml \
--format json
The resulting provider reports declared=true and
installed=enabled=ready=false. The normal CLI read also composes installed
providers from <runtime-root>/extensions/state.json, so the catalog and
runtime dispatch see the same active manifest revision. loopx extension
registers an already-installed subprocess entrypoint only after the manifest,
API, permission, and doctor checks pass. It does not download packages or grant
new permissions.
Starter And Scaffold¶
Create the next standalone extension through the same management surface. The
command previews by default and writes only with --execute:
loopx extension init loopx-example --format json
loopx extension init loopx-example --execute --format json
The default destination is packages/<extension-id>. Use --destination
when the provider is developed in another package or repository. The scaffold
creates an independently installable Python package, declarative manifest,
JSON stdin/stdout provider, versioned request and response JSON Schemas,
side-effect-free doctor, example request, and a short README. The generated
provider rejects missing, mismatched, or structurally invalid request contracts
before doing work. The init receipt explicitly identifies the starter as
standalone and names loopx extension run as its managed entrypoint.
extension init intentionally does not register a capability. It currently
generates only the complete standalone path. A [[provides]] extension needs a
real caller contract and command, while an [[implements]] extension needs an
existing capability-specific resolver, policy check, action/scope mapping, and
execution-envelope adapter. A generic scaffold cannot infer those authority
semantics safely. Add a capability integration profile first, then scaffold or
author the provider against that profile; do not add manifest tables that are
discoverable but not callable.
The command refuses every existing destination, including an empty directory; there is no force or merge mode. It also does not build, install, register, or enable the generated provider. Those remain explicit lifecycle steps so the package manager and LoopX activation state cannot drift behind one command:
Run all three commands from the same activated Python environment. LoopX
verifies the provider through its installed console entrypoint, so installing
the package into a different environment correctly fails with
entrypoint_missing.
python3 -m pip install packages/loopx-example
loopx extension install \
--manifest packages/loopx-example/extension.toml \
--execute \
--format json
loopx extension run loopx-example \
--input-json packages/loopx-example/examples/request.json \
--execute \
--format json
Treat the generated response as executable documentation, not a permanent domain contract. Before productizing the provider, replace the starter request, response, permission, and doctor semantics with bounded domain-specific ones.
Runtime Lifecycle¶
The lifecycle is local, explicit, and dry-run by default:
# Inspect the bundled OpenViking pilot, then activate it only if doctor passes.
loopx extension install \
--bundled openviking-semantic-preference \
--execute \
--format json
loopx extension list --format json
loopx extension doctor openviking-semantic-preference --execute --format json
loopx extension disable openviking-semantic-preference --execute --format json
loopx extension enable openviking-semantic-preference --execute --format json
# Activate the bundled Lark lifecycle provider before using lark-inbox.
loopx extension install --bundled loopx-lark --execute --format json
For a separately distributed provider, pass --manifest <extension.toml>.
upgrade validates and probes the new manifest before changing the active
revision. rollback probes the previous revision before switching back. A
failed probe leaves the current revision untouched. Activation state contains
validated manifest snapshots and revision ids in the private LoopX runtime
root; it does not contain provider output or credentials.
Standalone extensions use the same managed command shape as built-in
capabilities: LoopX accepts a bounded request, previews by default, executes
only with --execute, and returns a structured receipt. The v0 invocation
contract is:
The active manifest fixes the executable, arguments, protocol, permissions,
timeout, and revision. The caller supplies one JSON object over stdin and the
provider must return one JSON object over stdout. LoopX does not accept an
arbitrary executable path or argument passthrough. run never installs a
missing extension, and it rejects extensions with [[provides]],
[[implements]], or any declared permission; those providers are invoked
through their capability or domain command. Extension lifecycle management is
shared, but direct execution is reserved for zero-permission, runtime-only
standalone extensions.
Direct provider binaries are implementation and debugging surfaces; they are
not the supported management API.
The generic runner is deliberately non-effectful and grants no operation
effects. Both manifest permissions and runtime required_permissions must
be empty. Any operation needing read, write, send, publish, manage, or another
declared authority must enter through a capability or domain command that can
apply its domain policy before managed dispatch. Request files and stdin are capped while
being read. Provider stdout and stderr are drained concurrently and the provider
is started in a dedicated process group. Timeout or either output limit
terminates the entire group, so a descendant cannot continue effects after
LoopX reports that execution stopped.
Effectful capability dispatch uses
loopx_extension_execution_envelope_v0. The capability command, not the caller
or provider, creates this minimal envelope after resolving one enabled,
doctor-ready implementation and checking the domain activation policy. It binds:
- the exact action;
- structured effect scope;
- extension id and active manifest revision;
- a digest of the exact provider request, excluding the attached envelope.
The provider repeats this validation before any effect. A caller-supplied envelope, different request, wider scope, changed action, or mismatched active revision fails closed. Capability id, protocol, and permission remain authoritative in manifest resolution instead of being duplicated in the envelope. The envelope is request binding, not proof of issuer identity, a security token, or a replacement for service-side authentication and authorization.
disable is reversible, but enable never trusts an earlier readiness result:
it reruns the configured doctor and changes the enabled bit only after that
probe succeeds. A successful doctor binds readiness to both the active manifest
revision and the resolved runtime identity. Missing or replaced executables,
interpreters, or Python module sources fail closed until a new executed doctor
succeeds; a failed executed doctor clears the stale proof without switching
revisions.
An enabled implementation is resolved by capability id and versioned protocol, then checked against its declared permission, current revision, and current doctor proof. Callers do not need to copy an extension id into normal config. Disabled or stale implementations remain visible in the catalog but are not dispatch candidates. When multiple enabled, doctor-ready extensions implement the same capability/protocol pair, resolution fails closed until the caller selects the intended provider during migration. Domain config may add bounded provider arguments, but cannot replace the manifest entrypoint, timeout, protocol, or permission contract.
Compatibility delegates use the same revision-bound readiness rule. Every
configured loopx lark-inbox operation resolves the enabled loopx-lark
provider, its current doctor proof, and the permission needed by that operation
before entering the in-process provider code. Disabling the extension therefore
blocks new collector starts, drain, ingest, reply, and acknowledge operations;
upgrade and rollback affect new invocations without changing project
configuration. Extension lifecycle commands do not terminate an already
running host-managed collector process; stop or restart that supervisor service
separately when changing the active provider revision.
Quota and Turn composition apply the same read gate. They inject the Lark
extension's urgency projector only after resolving lark.inbox.read; provider
profile/chat schema and private config reads stay in the extension. If the
extension is missing, disabled, or stale, urgency is unavailable and cannot
activate a Lark work lane. This adds no agent-facing CLI arguments.
Placement Decision For Agents¶
Before creating a directory, LoopX or an executing agent must answer these questions in order:
- What user outcome and caller-visible contract is being added or changed?
Capability ids describe outcomes, not transports. Names such as
connector,provider,adapter, orsinkusually describe an extension or internal mechanism unless callers use and validate that mechanism as an independent product contract. If an existing capability already owns that contract, add the implementation toloopx/capabilities/<existing-capability>/instead of creating a sibling. - Must LoopX core always ship and maintain the implementation? If yes, it may be a built-in capability. A new built-in needs a stable id, a real entrypoint or protocol call site, focused validation, and catalog registration.
- Does the implementation need independent installation, enablement, disablement, upgrade, dependencies, credentials, or provider ownership? If yes, it is an extension provider. The capability remains the contract; the extension manifest declares that it provides the contract.
- Is this only registration or lifecycle machinery shared by all
extensions? Put that mechanism in
loopx/extensions/, not in a provider package. - Is this only an internal helper? Put it in the nearest module that owns its change reason. Do not register a capability or create an extension.
Use this placement map after answering the questions:
| Change | Placement |
|---|---|
| Existing built-in capability behavior | loopx/capabilities/<capability-id>/ |
| Built-in catalog and registration contract | loopx/capabilities/catalog.py or registry.py |
| Generic extension runtime | loopx/extensions/ |
| Co-located optional extension distribution | packages/<package-id>/ |
| Separately distributed extension/provider | owner package or repository |
| Internal implementation helper | nearest owning module |
Some work belongs on both axes, but an optional workflow does not need a capability merely because it is user-visible. Create a capability only when LoopX callers need a provider-neutral contract, catalog identity, and routing surface. An extension-owned command and packet contract may remain a standalone extension runtime. Finance value discovery uses this standalone shape; public-market, filing, and news collection can stay inside that extension until a real cross-provider LoopX contract exists.
value-connectors is an existing compatibility CLI and protocol surface. Do
not use it as the public capability owner for new work. Migrate each profile
to an existing outcome capability such as issue-fix or content-ops, or to
a standalone extension such as loopx-finance-value-discovery, before
retiring the compatibility surface. This keeps the migration
behavior-preserving instead of replacing one broad bucket with another broad
bucket.
Before editing, record a compact rationale in the active todo or plan:
capability_id: <existing-or-new-contract>
provider_id: loopx-core | <extension-id>
origin: builtin | extension
placement: <target-directory-or-package>
reason: <why the nearest existing owner is or is not sufficient>
Use capability_id: none for a standalone extension. Do not create a new
capability directory merely because no current directory has the feature name
or because the manifest needs a lifecycle anchor. Do not create an extension
merely because an external service is involved: a built-in connector can still
belong to an existing capability when it shares the core release and
lifecycle.
Manifest Contract¶
An extension manifest is declarative TOML. An executable [runtime] is enough
for a standalone extension. [[provides]] records add new capability contracts
to the catalog. [[implements]] binds a provider runtime to an existing
core-owned capability without duplicating that capability id. Do not add either
table solely to make a runtime installable.
The v0 runtime exposes integer extension API version 1 and accepts bounded
integer constraints such as >=1,<2; incompatible manifests fail closed.
schema_version = "loopx_extension_manifest_v0"
id = "loopx-lark"
version = "1.0.0"
requires_loopx_api = ">=1,<2"
permissions = ["read_status", "read_todos", "external_write"]
[runtime]
protocol = "lark_kanban_provider_v0"
python_module = "loopx.extensions.lark.provider"
doctor_args = ["--doctor"]
required_permissions = ["read_status", "read_todos"]
timeout_seconds = 30
[[provides]]
id = "lark-kanban"
kind = "projection_sink"
title = "Lark Kanban projection"
status = "active"
visibility = "public"
real_world_anchor = "operator-facing Lark Base projection"
user_value = "Project public-safe LoopX status and todo rows into Lark."
entry_command = "loopx lark-kanban sync"
next_real_step = "Validate one explicitly enabled owner-approved sink."
The bundled OpenViking pilot uses [[implements]] instead:
[runtime]
protocol = "semantic_preference_provider_v0"
entrypoint = "loopx-openviking-semantic-preference"
doctor_args = ["--doctor"]
required_permissions = ["semantic_preference.read"]
[[implements]]
capability_id = "semantic-preference"
protocol = "semantic_preference_provider_v0"
The bundled periodic-report archive uses the same ownership direction. It implements one existing capability port rather than registering a second "OpenViking report" product capability:
[runtime]
protocol = "periodic_report_sink_v0"
python_module = "loopx.extensions.openviking_periodic_report.provider"
required_permissions = ["openviking_context_write"]
[[implements]]
capability_id = "periodic-report"
protocol = "periodic_report_sink_v0"
Its capability-specific activation wrapper additionally requires an enabled
periodic_report_activation_v0, a matching non-disabled sink binding, and the
observed openviking_context_write runtime capability. Those project and turn
facts do not belong in the generic extension manifest or lifecycle state.
Finance value-discovery sample¶
packages/loopx-finance-value-discovery/ is a co-located, independently
packaged standalone workflow. Its manifest registers only the
finance_value_discovery_extension_v0 runtime; it does not create a capability
catalog entry or a value-connectors route. After an explicit install and
successful doctor probe, invoke it through the managed extension command:
loopx extension install \
--manifest packages/loopx-finance-value-discovery/extension.toml \
--execute
loopx extension run loopx-finance-value-discovery \
--input-json packages/loopx-finance-value-discovery/examples/paypal-debeta-discovery.json \
--execute \
--format json
The included PayPal packet preserves a reusable de-beta research method, not an investment conclusion: start from a frozen cross-sectional screen, retain same-group controls, separate structural growth from profit-pool capture, require dilution and terminal-risk evidence, then falsify the candidate before selecting at most one successor. The reducer performs no live reads, gives no price target or advice, and cannot trade or start a continuous watch.
For upgrade compatibility, the retired
value-connectors Finance selectors, including the legacy
plan --connector-id finance_market_snapshot form, remain as migration
tombstones. They return value_connector_extension_migration_v0 with ordered
extension startup prerequisites; they do not execute Finance or restore a
Finance capability. Source checkouts can install the co-located provider package
before registration. Packaged LoopX users still need a separately distributed
provider artifact, so agents must stop rather than claiming automatic
installation when that artifact is unavailable.
Runtime-required permissions must be a subset of the provider's declared permissions. Declaring either does not grant authority: existing LoopX goal boundaries, user gates, and external-write authorization still decide whether an operation may execute. Extension packages are trusted executable code rather than an operating-system sandbox; the manifest records and constrains managed routing, but cannot make an untrusted provider safe.
Every executable runtime declares exactly one launch target. Use entrypoint
for a separately installed executable such as the OpenViking provider. Use
python_module for a provider shipped in the LoopX Python package. Module
providers run as <current-loopx-python> -m <module> and their doctor proof is
bound to both that interpreter and the resolved module source. This lets a
clean source checkout and a local LoopX release activate bundled providers
without separately installing a console script; catalog discovery remains
declarative and does not import the module.
Scope Boundaries¶
The executable v0 runtime intentionally does not:
- rename or move existing capability implementation directories;
- infer capabilities from Python packages;
- download, build, or install extension packages;
- start services, create credentials, or edit provider configuration;
- import an extension entrypoint during catalog discovery;
- let manifest permissions bypass LoopX control-plane authority.
These boundaries keep activation reversible and auditable while leaving package distribution and service setup to explicit operator-owned workflows.
Provider migration follows the same direction. Core routing consumes compact
provider-neutral read models, while provider packages own collection, transport,
credentials, and external effects. For example, quota reads
operator_inbox_urgency_v0 through an injected projector. The generic parser
and read-model contract stay in the control plane; Lark schema, identity,
destination, collection, reply transport, and provider-owned configuration live
under loopx/extensions/lark/. The existing
loopx lark-inbox command remains a direct compatibility delegate, but it now
requires an installed, enabled, doctor-verified loopx-lark revision with the
operation's declared permission. The provider subprocess currently implements
doctor only; command execution remains in-process until the transport protocol
is migrated.
The former loopx.capabilities.lark provider imports are intentionally removed
instead of kept as wrappers. Lark Kanban and Explore presentation sinks live
under loopx.extensions.lark.presentation; their compatibility CLI delegates
require the installed, enabled, doctor-verified revision to declare
lark.projection_sink.use. No additional agent-facing CLI arguments are
required.