Build a standalone Extension
This chapter creates loopx-text-stats from the official LoopX scaffold. The official scaffold supplies the complete runnable baseline; this chapter provides the narrowed manifest, request and response contracts, core function, and validation steps without a separate exercise repository.
Observable success
At the end:
- the scaffold is an independent Python package;
- the manifest uses
loopx_extension_manifest_v0; - request and response each have a versioned JSON Schema;
- the Provider reads one JSON object from stdin and writes one JSON object to stdout;
- doctor has no side effect;
- invalid input fails closed before computation;
- both manifest and runtime declare zero permissions.
1. Generate the official scaffold
From a workspace where you want to build the example:
loopx extension init loopx-text-stats \
--destination standalone-extension \
--execute \
--format jsonextension init previews by default. --execute is required to write files, and the destination must not already exist, even as an empty directory. The command does not build, install, register, or enable the package.
The generated path is:
standalone-extension/
├── extension.toml
├── pyproject.toml
├── README.md
├── examples/
│ └── request.json
├── schemas/
│ ├── request.schema.json
│ └── response.schema.json
└── src/
└── loopx_text_stats/
├── __init__.py
└── cli.pyThis is a complete standalone path. It does not invent the Capability authority required for [[provides]] or [[implements]].
2. Read the manifest as a contract
After narrowing the generated scaffold to this example, the manifest is:
schema_version = "loopx_extension_manifest_v0"
id = "loopx-text-stats"
version = "0.1.0"
requires_loopx_api = ">=1,<2"
permissions = []
[runtime]
protocol = "loopx_text_stats_extension_v0"
entrypoint = "loopx-text-stats"
doctor_args = ["--doctor"]
required_permissions = []
timeout_seconds = 30The important constraints are:
idis the lifecycle identity;versionparticipates in revision and upgrade;requires_loopx_apideclares the compatibility window;protocolis the Provider wire contract;entrypointmust exist onPATHin the Python environment running LoopX;doctor_argsnames a read-only readiness probe;- both permission lists are empty;
- the managed runtime fixes the timeout rather than accepting an arbitrary caller override.
3. Define a bounded request
The example request is:
{
"schema_version": "loopx_text_stats_request_v0",
"text": "LoopX keeps project state explicit.\nExtensions keep delivery lifecycle explicit."
}The request schema requires:
- an object payload;
- an exact
schema_version; - a
textstring containing a non-whitespace character; additionalProperties: false.
Rejecting unknown fields protects the permission boundary. If the caller sends:
{
"schema_version": "loopx_text_stats_request_v0",
"text": "hello",
"path": "/tmp/input.txt"
}the Provider must reject it. It must not reinterpret path as file-read authority.
4. Implement pure computation
The example's core function is:
def analyze_text(text: str) -> dict[str, int]:
return {
"characters": len(text),
"non_whitespace_characters": sum(
1 for character in text if not character.isspace()
),
"words": len(re.findall(r"\S+", text)),
"lines": len(text.splitlines()) or 1,
}It is a good first standalone Extension because it is deterministic, reads no environment or files, uses no network, modifies no external system, and does not depend on LoopX project state.
The Provider validates structure before computation and returns errors through a versioned response:
{
"ok": false,
"schema_version": "loopx_text_stats_response_v0",
"extension_id": "loopx-text-stats",
"error": "extension input has unsupported fields ['path']"
}Do not expose tracebacks, environment variables, or local paths in public receipts.
5. Define the response contract
The stable domain response is:
{
"ok": true,
"schema_version": "loopx_text_stats_response_v0",
"extension_id": "loopx-text-stats",
"request_schema_version": "loopx_text_stats_request_v0",
"result": {
"characters": 80,
"non_whitespace_characters": 71,
"words": 10,
"lines": 2
}
}The response schema uses oneOf to separate success and failure. Tests should assert this domain contract, not every field in the outer LoopX CLI receipt. That allows additive receipt changes in a minor release without breaking a domain test.
6. Keep doctor free of effects
The starter doctor path is:
if args.doctor:
return 0For this pure Provider, readiness means the entrypoint starts and parses arguments. Doctor must not:
- create files;
- access the network;
- write credentials;
- change Extension state;
- perform a business effect;
- emit unbounded logs.
A real Provider may perform bounded read-only dependency checks. Readiness still needs to be repeatable and effect-free.
7. Install the package and run tests
Use one Python environment for the Provider and LoopX:
cd standalone-extension
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -e '.[test]'
python3 -m pytestLoopX validates the installed console entrypoint. If the package lives in another virtual environment, entrypoint_missing is the correct result; LoopX must not search arbitrary source directories.
Common mistakes
Hand-writing a smaller scaffold
This often omits schema, doctor, compatibility, or the package entrypoint. Generate the complete official path first, then make minimal domain changes.
Accepting arbitrary keyword arguments
This destroys the bounded request and can expand authority accidentally. The JSON Schema and Provider validation should both fail closed.
Running business work in doctor
Doctor proves readiness. It does not authorize an effect. Business requests belong in the managed runtime or an authorized Capability/domain command.
Adding a permission for demonstration
extension run rejects a permissioned Extension. Design the real Capability and authority before building an effectful Provider.
