# Axiom Extension Developer Guide

**Status:** Working doc — practitioner companion to ADR-044 (extension distribution model). Living document; updates as the extension surface matures.

This guide is for anyone building an Axiom AEOS extension. It assumes you've already decided you need to build *something* — what it's for, who'll use it. It does not tell you how to write Python. It tells you the shape an extension takes once you commit to writing one.

---

## Why bother — what Axiom does for the tools it wraps

Before the mechanics: be clear-eyed about why an extension is worth shipping at all. Per ADR-044 D6, Axiom changes what *any* tool it wraps becomes, in four concrete ways:

> a. **Way easier to install** — `pip install <ext>` is the integration step.
> b. **Way more flexible in form** — bare-metal, container, SSH-to-peer, federation-dispatched, all from the same wrapper.
> c. **More integrated** — the wrapped tool automatically composes with provenance, federation routing, classification gates, live observability, signed receipts, RACI-managed automation.
> d. **More useful as a standalone entity** — even with no Axiom-aware consumer present, the wrapper acquires compositional value the moment any Axiom env exists on the host. Default-installed Axiom agents (M-O hygiene, AXI chat, PRT publishing, D-FIB diagnostics) auto-engage with the wrapped tool for free.
>
> *Axiom 'breathes life' into an otherwise lifeless digital organism. Axiom is the neural spark, nervous system, and heartbeat to the host it inhabits.*

When you sit down to design your extension, this is the lens. Don't think of yourself as adding more buttons to the wrapped tool's CLI; think of yourself as plugging the tool into a nervous system that already runs in every Axiom env. The agents are already there; the federation is already there; the classification gates are already there. Your job is to wire the tool into those, not to recreate them.

---

## Step 1 — apply the standalone-vs-builtin rule

Per ADR-044 D1, every new extension is either standalone or builtin. Ask:

> *"Is this extension useful for ALL facilities/instances within the consumer's domain AND useful across ALL backends/codes/sources within that domain?"*

| Answer | Distribution |
|---|---|
| **Yes (universal within the domain)** | **Builtin** to the consumer's main repo |
| **No (specific to one code, backend, source, or facility)** | **Standalone** repo |

Bias toward **standalone** when in doubt. Per ADR-044 D6, standalone wrappers reach every Axiom-enabled host; builtins only reach the consumer they ship with.

Examples to calibrate against:

| Extension | Outcome | Reasoning |
|---|---|---|
| Hygiene / drift dashboard | builtin (axiom-os) | Every consumer benefits regardless of domain |
| Classroom (lecture/quiz/cohort) | builtin (Keplo) | Every classroom consumer needs it |
| OpenMC physics-code adapter | standalone (`UT-Computational-NE/axiom-ext-openmc`) | Specific to one physics code |
| Canvas LMS adapter | standalone | Specific to one LMS |

---

## Step 2 — set up the package

### If standalone (the more common case after ADR-044's tilt)

1. **Create the repo** under the consumer-domain GitHub org. For nuclear: `UT-Computational-NE/<ext-name>`.
2. **License + NOTICE.** Apache-2.0 is required. NOTICE captures joint UT + B-Tree Labs copyright + external-tool attribution + trademarks.
3. **`pyproject.toml`** declares:
   - `name`: `axiom-ext-<tool>` (preferred convention) or `<consumer>-ext-<tool>` for non-axiom-side wrappers.
   - `version`: starts at `0.1.0`. Independent of any other package's version.
   - `dependencies`: pin `axiom-os-lm >= <minimum-required-feature-version>`. For most extensions today: `>= 0.15` (entry-point adapter discovery).
   - `[project.entry-points."axiom.compute.adapters"]` (or another appropriate group — see Step 3) — *this is what makes the wrapper auto-discoverable*.
4. **AEOS manifest** at the package root: `axiom-extension.toml` declaring `name`, `version`, `kind`, `provides`, etc. per AEOS spec §5.
5. **CI workflows** — `lint`, `unit-tests` (3.11 + 3.12 + 3.13), and `publish.yml` (tag-push triggers PyPI publish via OIDC trusted publisher).
6. **PyPI** — set up org-level pending-publisher under the consumer-domain's PyPI org. Owner field = the GitHub owner of the workflow.

The reference implementation is `https://github.com/UT-Computational-NE/axiom-ext-openmc` (extracted 2026-05-05 per ADR-044 P1).

### If builtin

1. Place under `<consumer>/src/<consumer>/extensions/builtins/<name>/`.
2. AEOS manifest still required (`axiom-extension.toml`).
3. No own PyPI release; ships with the consumer's release cadence.
4. No own GitHub repo / no GitLab mirror.

---

## Step 3 — pick the right entry-point group

Entry-points are how `pip install <your-ext>` becomes "and now Axiom finds it." Choose the appropriate group based on what your extension provides:

| Capability kind | Entry-point group | Example |
|---|---|---|
| Compute kernel adapter (one-per-physics-code) | `axiom.compute.adapters` | `openmc = "axiom_ext_openmc.adapter:OpenMCKernelAdapter"` |
| (future — hooks-not-yet-defined) Storage backend | `axiom.storage.providers` | TBD |
| (future) LMS adapter | `axiom.classroom.lms_adapters` | TBD |
| (future) Signal source extractor | `axiom.signals.extractors` | TBD |

If your extension's capability doesn't have an entry-point group yet, propose one in your PR. New groups are added by amendment to the AEOS spec.

---

## Step 4 — verify the four-form install matrix (the standalone behavior contract)

Per ADR-044 D3, every standalone extension MUST install correctly + behave correctly in:

| Form | Verify with |
|---|---|
| **Bare metal** (underlying tool's native binary on PATH) | The wrapped tool's traditional surface — both CLI and Python API — is unchanged. The wrapper does not shadow the tool's namespace. |
| **Container** (Docker / no native binary) | The adapter's containerized runner pulls the official upstream image and dispatches to it. |
| **Axiom-enabled environment** | `axi <verb>` discovers the adapter via entry points; the bonus value (live dashboard, signed receipts, federation routing) is available with no additional configuration. |
| **GitHub-to-GitLab mirror sync** | Both remotes carry the same tags + releases. (Skip for v0.1.0 if the rule's Phase A migration explicitly defers it.) |
| **PyPI resolvable** | `pip index versions <package>` returns the latest tag's version within minutes of publish. |

If any of these fail on your extension, it's not v1-ready. Add release-blocker checklist items mirroring this matrix.

---

## Step 5 — what makes an extension "agent-friendly"

Beyond the basic discovery + behavior contract, extensions can opt into deeper agentification — the "neural spark" surface area of the platform. This is where ADR-044 D6's normative claim becomes operational. Today the following surfaces exist:

| Default agent | What it auto-does for your extension if you wire in |
|---|---|
| **M-O (hygiene)** | Resource cleanup, scratch retention, drift detection on extension state |
| **AXI (chat)** | Conversational discovery + invocation of your extension's verbs |
| **PR-T (publishing)** | Output-document lifecycle: signed artifacts → drafts → published |
| **D-FIB (diagnostics)** | Failure-pattern recognition + remediation prompts when your extension errors |

Wiring an extension into these is currently per-agent and partly conventional; ADR-044 D6's "open sub-questions" call out the need for a unified opt-in surface. For now, follow the patterns these agents already use in builtin extensions; when in doubt, ask in a PR.

---

## Step 6 — release + maintain

### Tagging + publishing

```bash
# In your standalone repo:
git tag -a v0.1.0 -m "Initial release."
git push origin v0.1.0
# → triggers .github/workflows/publish.yml → builds → PyPI publish via OIDC
```

The publish workflow's `post_publish_smoke` job hard-fails if PyPI doesn't index the new version within 5 minutes. That's the desired loud-signal behavior — silent skip is not an option.

### Versioning

Semver. Minor for additions (new runners, new references, new facility packs); major for adapter-interface changes; patch for everything else. Independent of axiom-os-lm's version, but pin against axiom-os-lm's minimum-required-feature-version in your `dependencies`.

### Mirror discipline

Per ADR-044 D2, GitLab mirror is push-driven from GitHub. A stale mirror is worse than no mirror — consumers think they have current code. If you skip the mirror for v0.1.0 (per ADR-044's Phase A migration footnote), capture that decision explicitly so the gap doesn't drift.

---

## Pitfalls collected from real extractions

### Pitfall: shadowing the wrapped tool's namespace

The wrapper must not import the wrapped tool's top-level module unconditionally at package-level. If it does, `import openmc` (the user's traditional path) silently routes through the wrapper, breaking the "Axiom-oblivious" form. Keep the wrapper's `__init__.py` minimal; import the wrapped tool only inside functions that actually invoke it.

### Pitfall: explicit `register_adapter()` at module-level + entry points

If your adapter both calls `register_adapter("foo", FooAdapter())` at the bottom of its module *and* declares an entry point, you'll register twice (harmless) but trip ruff's E402 ("module level import not at top of file"). Once `axiom-os-lm >= 0.15` is the minimum, **drop the explicit `register_adapter` call** — entry points handle it. This is observed in `axiom-ext-openmc`'s extraction history.

### Pitfall: forgetting the AEOS manifest

`test_all_builtins_have_manifests` (in axiom-os) hard-fails the release CLI if any builtin lacks `axiom-extension.toml`. Add yours from the start; don't wait for a release-time scramble. Standalone extensions also need it for AEOS conformance even though there's no centralized test in axiom-os covering them.

### Pitfall: pinning axiom-os-lm too loosely

If your extension uses a feature that shipped in axiom-os-lm `0.15.0`, pin `axiom-os-lm >= 0.15`. Pinning `>= 0.12` (an older minimum) means users on 0.13.0 install your package successfully but it crashes at runtime — terrible UX. Check the changelog before committing to a minimum.

---

## Cross-references

- **ADR-018** — each physics code is its own AEOS-conformant extension; physics = native
- **ADR-031** — extension self-containment (docs + tests co-located)
- **ADR-040** — compute decomposition primitive (consumer of adapter entry points)
- **ADR-043** — RACI evolution (graduated agent autonomy, applies to extension-managed actions)
- **ADR-044** — extension distribution model (standalone-or-builtin rule + the canonical thesis above)
- **AEOS spec** — `docs/specs/spec-aeos-0.1.md`, `docs/specs/spec-aeos-1.0.md`
- **Reference standalone repo** — `https://github.com/UT-Computational-NE/axiom-ext-openmc`
