The left4me bundle's first cut had two reactors that returned static dicts without calling metadata.get(...): systemd_services (enable/run flags) and nftables_output (two static rule strings). Both passed bw test (no consumer yet). Once attached to ovh.left4me, bw raised "did not request any metadata, you might want to use defaults instead". Fix was to fold both into defaults. Document the pitfall, with the verbatim error wording and the note that this applies to cross-namespace contributions too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 KiB
bundles/
Before you start
Read docs/agents/conventions.md first
— it covers vault calls, demagify, the repo.libs.<x> helpers, and the
files agents must not modify. Skipping it leads to subtly broken bundles
(vault calls in the wrong place, dict-in-set TypeError because of
unhashable nesting, etc.).
For bundlewrap-language reference (item types, dep keywords,
metadata_reactor, defaults, item-file template syntax) see the fork's
AGENTS.md
and its docs/content/guide/item_file_templates.md.
What's here
103 bundles. Each is a directory bundles/<name>/ containing some of:
bundles/<name>/
├── items.py # the items this bundle creates (files, services, packages, …)
├── metadata.py # `defaults` + `@metadata_reactor` functions
├── files/ # static or templated file payloads referenced from items.py
└── README.md # one doc per bundle, for humans and agents (see "Per-bundle README" below)
Conventions
- Bundle names are lowercase, hyphen-separated:
backup-server,bind-acme,dm-crypt. No underscores in new bundle names — seeconventions.md#naming-conventions. items.pyis plain Python; it producesfiles = {...},pkg_apt = {...},svc_systemd = {...}, etc. dicts at module scope. Cross-item dependencies useneeds/triggers/triggered_by— see the fork'sAGENTS.mdfor the full keyword cheat sheet.metadata.pyusesdefaults = {...}for static seed values and@metadata_reactor.provides(...)for derived values. Reactors are pure functions of(metadata,)— no side effects, no I/O.- Helpers go in
libs/when they're useful to more than one bundle. Don't duplicate logic across bundles. - Custom item types (e.g.
download:) live initems/, not per-bundle. - Bundles own application-wide knowledge; nodes carry only the few
per-host knobs the bundle actually needs. When designing a bundle,
identify the per-node knobs (e.g. domain, uplink interface, a
vault-id suffix) and put everything else in
defaults, or in a reactor that derives from those knobs. Per-node random secrets belong indefaultsviarepo.vault.random_bytes_as_base64_for(...)keyed on the node — not in the node file. Seebundles/left4me/metadata.py:10(secret_keyderived in defaults) andbundles/postgresql/metadata.py:4(vault-derivedpassword_forat module scope).
How to add a new bundle
-
mkdir bundles/<name>/(lowercase, hyphenated). -
Write
items.pyand (if anything is configurable)metadata.py. Userepo.libs.hashable.hashable(...)when you need to nest a dict or set inside a metadata set; raw dicts/sets aren't hashable. -
Drop static payloads into
bundles/<name>/files/. For Mako-templated files, declare'content_type': 'mako'on thefile:item — see the fork's item-file-templates guide. -
Wire to nodes. Either add an entry to the relevant
groups/<axis>/<x>.py(preferred for shared bundles) or to the node'sbundleslist directly (nodes/AGENTS.md). -
Verify, in this order:
bw test— repo-wide parse + cross-cutting hooks. Loads every bundle, but reactors don't fire for nodes that haven't opted into the bundle yet — bugs in new reactors stay hidden here.- Attach the bundle to a node (via the node's
bundleslist, or a group it belongs to). Until you do, the next steps don't actually exercise the bundle. bw test <node>— exercises every reactor and item-graph edge for that node. This is where most new-bundle bugs surface.bw items <node> --blame— confirm items materialise with the right paths, authored by the expected bundle.bw metadata <node> -k <a/b>— spot-check derived metadata.bw hash <node>— preview vs current host state.
See
docs/agents/commands.md#bundle-validation-workflowfor the rationale. -
Add a
bundles/<name>/README.md. See "Per-bundle README" below for what to cover.
How to remove a bundle
git grep '<name>'innodes/,groups/, and otherbundles/to find references.- Remove those references.
rm -rf bundles/<name>/.bw testandbw nodesto confirm clean.
Pitfalls
metadata.pyis evaluated at load time for every node, every invocation ofbw. Heavy work or I/O slows the whole repo. Keep reactors pure and fast; pre-compute inlibs/if you must.- Static files vs templates.
bundles/<x>/files/<f>is static unless the matchingfile:item declarescontent_type='mako'(or a templating extension triggers it). To check, read the matchingfile:entry initems.py. - Reactors writing across namespaces. Some bundles' reactors write
into other bundles' metadata namespaces (e.g.
nextcloudwrites intoapt.packages,archive.paths). When you change such a bundle, every consumer's metadata changes too. The bundle'sREADME.mdoften calls these out — but the authoritative source ismetadata.pyitself; grep'<other-bundle>':in the reactors when in doubt. bw hashdoesn't accept selectors. Usebw hash <node>per literal name; see the fork's runbook.- Reactors must read metadata. If a reactor body returns a static
dict without calling
metadata.get(...), bw raisesValueError: <reactor> on <node> did not request any metadata, you might want to use defaults insteadonce a node consumes the bundle. Fix: fold the contribution intodefaults. The rule applies even when the reactor writes into another bundle's namespace — a static contribution to e.g.nftables/outputbelongs indefaults, where bw merges it with other bundles' contributions.
Per-bundle README
Each bundle has (or should have) a README.md. One doc per bundle,
written for humans and agents both. There's no fixed structure —
match the bundle's actual surface, write what helps a future reader
(or future you) avoid trial-and-error.
The existing READMEs vary in quality and shape. For orientation, look at the bigger ones, not the two-line ones:
bundles/flask/README.md— title + one-sentence purpose, a metadata example as a Python dict, then the contract the consuming git repo has to satisfy + a logging pitfall. The closest thing to a "balanced doc" in tree.bundles/dm-crypt/README.md— same shape, shorter: purpose + metadata example + one sentence on effect.bundles/apt/README.md— relevant upstream URLs at the top, then a Python metadata example with rich inline comments (type / optionality / where keys come from).bundles/nextcloud/README.md— operational scratchpad: iPhone-import recipe, preview-generator commands, reset queries. Captures muscle-memory the maintainer would otherwise re-learn each time.
Useful things to include, when relevant:
- A sentence or two on what the bundle does and when you'd attach it.
- A metadata example as a Python dict literal, with
#comments on each key (type, required vs default, units, where it comes from). This is the cleanest way to communicate the schema and matches howmetadata.pyactually looks. - Anything non-obvious about wiring it up — required keys without defaults, group-membership expectations, manual one-time steps.
- Cross-namespace metadata writes, when this bundle's reactors populate another bundle's namespace. Easy to miss, cheap to flag.
- Gotchas, debug recipes, failure modes you've actually hit.
What to skip:
- An exhaustive item list —
items.pyis shorter and more accurate. - Anything that would just rot — version numbers, "TODO" lists, change notes. Use git history.
If a single paragraph is enough to say what's worth saying, write a single paragraph. Verbosity isn't a goal.
Convention going forward is leave-as-you-go: any time you materially edit a bundle, top up its README (or write one if it's missing). Don't burn a session bulk-reformatting the existing ones — uneven quality is part of what we accept in exchange for not blocking other work.
See also
docs/agents/conventions.md— repo idioms (vault, demagify, naming, do-not-touch list).docs/agents/commands.md— repo-specific command deltas.items/AGENTS.md— custom item types (download:); when to write a new one vs usefile:.libs/AGENTS.md— shared helpers.- Fork's
AGENTS.md— bundlewrap-language reference + safety envelope.