Primitive spec

Pirenne

Tenant-as-code. Pirenne turns a tenant’s live configuration into a single portable document — a bundle — and turns a bundle back into a tenant. “What is this business?” and “make a business like this one” become the same artefact.

Status: Live on production since September 3, 2026 — export, plan/diff, section-by-section apply, review-gated history with rollback, ten starter verticals, and scenario runs. Since October 1, 2026 a bundle also carries the website and its pages. A tenant's Arkady rates and limits export with the bundle.

What it owns

An agent that configures a business by choreographing fifty sequenced write calls is unreviewable, unrepeatable and unsafe: a human has to supervise every call, a failure halfway leaves an undescribed half-built tenant, and nothing can be diffed or rolled back. A bundle inverts that. The agent emits one document; the platform turns it into a plan a human approves once, and applies it section by section, each section in its own transaction.

Pirenne owns the bundle format, the export that produces one, the diff that compares one against a live tenant, the apply that executes it, and the history that lets you go back. It owns no business rules — every section writes through the owning primitive’s normal surface.

Concepts

TenantBundle
Twenty-three sections — Daneel actions, Radiant repositories and assets, the website and its pages, Trantor resource types and resources, the Hardin catalog (tax classes, categories, modifier groups, products, pricing rules), Terminus groups, tiers and tier definitions, Magnifico audiences, Seldon schemes, offers and access rules, Speaker routing, promo codes, Arkady usage rates and limits, and assistants. Emitted as canonical YAML with a digest.
Handles, not ids
Every row carries a handle slugged from its natural key, and every reference names a handle, so a bundle applies unchanged to a different tenant. Ids buried in payloads — a booking-calendar block naming its offer — are rewritten to @ref: sentinels, and a final sweep reports any raw id that survived, so an unportable bundle says so instead of looking clean.
Config only, by construction
The catalogue is an allowlist: a column is in a bundle because someone named it. No people, no memberships, no bookings, orders or invoices, no credentials. The website travels as its layout, theme and pages, but never its address: applied to another business, the site lands at that business’s own address, and a custom domain is never carried, since ownership has to be proven by the business that holds it. A redaction pass would have the opposite failure mode — an allowlist’s worst case is a missing field, not a leaked one.
Plan and pin
tenant.plan_bundle produces the minimal diff of bundle-versus-live: creates and field-level updates, with live rows the bundle no longer describes reported as orphans (never deleted), and hard-to-take-back changes flagged and explained. It is a read — nothing is written. Pinning stores it and returns a planId, and apply executes that plan exactly as shown or refuses.
History and rollback
Every apply lands in a history: digest, counts, errors, who did it, the plan it came through, and whether it can be restored. Rollback re-applies an earlier document: it overwrites the fields both documents name, and cannot remove anything added since.
Starter bundles
Ten working businesses ship as ordinary bundles — racquet club, gym, salon, studio, restaurant, barbershop, spa, clinic, hotel, music school. Each is shaped differently on purpose because the verticals are: the racquet club books resources by the hour, the gym runs a timetable needing a room and a trainer, the salon has two appointment lengths where the long one takes a deposit, and the studio is capacity-shaped rather than resource-shaped. Applying one walks the same sections and rolls back the same way; nothing about a starter is special once applied.
Scenarios
Each starter ships its own scenario as its acceptance test. A scenario runs in sandbox: outbound drivers (messages, payments, carriers) are neutralised, but its steps write real sandbox-tagged rows into the tenant. It returns a structured verdict — so, once a human approves the run (on the agent surface it is tier 2), an agent can prove the business it just built actually works: courts to book, an offer to book them under, a customer record to book them for.

API surface

The reads are scoped to tenant configuration and gated to admin views — a location-scoped operator has no business exporting the tenant’s whole configuration. Every write above export is tier 2, which on the agent surface means an agent can only ask.

RoutePurpose
GET /pirenne/v1/tenant/describeCounts per section plus a shallow list of offers, schemes, resource types and products, each with its handle. The orientation read an agent makes first.
GET /pirenne/v1/tenant/exportThe full bundle as canonical YAML plus its metadata block. Optional ?sections= filter.
POST /pirenne/v1/tenant/planThe bundle-versus-live diff. A read; pin: true stores it and returns a planId.
POST /pirenne/v1/tenant/plans/{planId}/applyExecute a pinned plan exactly as shown, or refuse.
POST /pirenne/v1/tenant/applyCreate or update everything a bundle names. dryRun supported.
GET /pirenne/v1/tenant/historyWhat has been applied, newest first, and whether it can be restored.
POST /pirenne/v1/tenant/history/{id}/rollbackRe-apply an earlier document.
GET /pirenne/v1/tenant/startersThe ten shipped verticals and the variables each needs.
POST /pirenne/v1/tenant/starters/{key}/applySet the tenant up from a starter.
POST /pirenne/v1/tenant/scenarioRun a scenario in sandbox (drivers neutralised, sandbox-tagged rows written); returns a structured verdict.

How it fits with the rest

flowchart TB
  Live[Live tenant] --> Exp[tenant.export]
  Exp --> B[TenantBundle
YAML + digest] B --> Plan[tenant.plan_bundle
diff, no writes] Plan --> Human{Human approves} Human --> Apply[tenant.apply_plan] Apply --> Prims[Seldon / Trantor / Hardin
Terminus / Radiant / Daneel] Apply --> Hist[History + rollback] Start[Starter bundle] --> Plan Apply --> Scen[Scenario run
sandbox]

Pirenne writes through each primitive’s normal surface, so a bundle cannot do anything an operator could not. Gaal is what makes it matter for agents: an outside agent proposes one document instead of fifty writes, and a human approves it once. The neutralised side effects during a scenario run come from the same sandbox posture Demerzel exposes to operators.