Skip to content
Docs menu

The starter library

A new tenant should not start from an empty config. Tempa publishes a curated library of packs: versioned sets of standards, skills, commands, agents, policy bundles and MCP templates, each with a changelog (TEMPA-117, epic TEMPA-E11). A tenant subscribes to a pack on a node and pins a version; a new version comes in as a reviewable proposal and never changes a live profile on its own (Subscriptions, TEMPA-118). The starter packs are in this repo under library/packs/ (below, TEMPA-119).

The pack format

A pack is a folder with the layout of a config repo's assets (config.md), plus its manifest:

pack.yaml                   id, name, publisher, version, changelog, requires, contents, defaults
README.md                   optional
standards/<name>.md
skills/<name>/SKILL.md      Agent Skills format; other files in the folder ship with it
commands/<name>.md
agents/<name>.md            frontmatter needs a description
policies/<bundle>/*.rego    package name = folder name
mcp/<name>.yaml             an MCP template (below)

Nothing else: no tenant.yaml, nodes/, steps.yaml or CODEOWNERS (a pack is never one tenant's config), no links, no other files.

pack.yaml

id: python-fastapi               # slug, unique on the installation
name: Python and FastAPI
description: Standards, a TDD skill and uv guardrails for Python services.
publisher: tempa                 # tempa: the installation; a partner tenant's id for its packs (TEMPA-169)
version: 1.2.0                   # semantic version, no build metadata
changelog:                       # newest first; the first entry is this version
  - version: 1.2.0
    date: 2026-11-01
    changes: [Add the Sentry MCP template]
  - version: 1.1.0
    changes: [First release]
requires:
  tempa: 0.1.0                   # the oldest Tempa the pack works with
contents:                        # must match the pack's files exactly
  standards: [python-style]
  skills: [tdd-workflow]
  commands: [review]
  agents: [reviewer]
  policies: [python_uv]
  mcp: [sentry]
defaults:                        # optional: settings a subscriber can adopt, as in a node.yaml
  settings:
    standards: { mode: extendable, add: [python-style] }
    policies: { mode: locked, bundles: [python_uv] }

schema/pack.json describes pack.yaml (for editors; make schema generates the Python model tempa_api.schema.Pack and the TypeScript type Pack from it). Default settings name only the pack's own assets and use the settings and modes of a node; work_board is never a default (naming people needs the tenant's own DPIA).

MCP templates

An MCP endpoint in a pack is a template: the subscriber fills its placeholders and stores its credential.

name: sentry
description: Sentry issues and events for the team's projects
url: "https://{{sentry_host}}/mcp"
auth: none                       # or tempa-device
tools: [list_issues]             # optional allowlist
placeholders:
  sentry_host: { description: Your Sentry host, example: sentry.io }
credential:                      # optional: the subscriber stores it as the mcp_credential:<name> secret
  description: A Sentry user auth token with the project:read scope

The URL is https://; every {{placeholder}} in it is declared and every declared one is used; its host is a placeholder or a public host name (no localhost, *.internal and the like, no credentials in the URL).

Validation

tempa_config validates a pack with the config repo's own asset loaders (frontmatter, skill names, Rego that compiles with its package matching the folder) plus the pack's rules: the manifest's schema, the changelog, the contents list against the files, the default settings, the MCP templates, the layout, and nothing tenant-specific: no tenant config, no Entra ids (a GUID in any file is refused) and none of the words given with --forbid (whole words, any case: for example the name of the organisation the pack was taken from). The config store's caps apply: a file at most 2 MiB, a pack at most 32 MiB and 5000 files.

cd api
uv run python -m tempa_config validate-pack ../path/to/pack --forbid devleaps
uv run python -m tempa_config pack-archive ../path/to/pack... --out ../dist --forbid devleaps   # <id>-<version>.tar.gz each

Versions and visibility

  • Immutable versions. A published version never changes: the database refuses any change to a version or its files (a trigger). Publishing the same version with the same files again changes nothing; with other files it is refused (409): publish a new version.
  • Yanking marks a version not installable, with a reason everyone sees. It stays readable for whoever has it. latest is the newest version that is not yanked (semantic version order, a pre-release before its release).
  • Visibility per pack: public (every tenant), private (its publisher only: for an installation pack, the operators) or partner_clients (a partner's pack for the tenants it manages, TEMPA-168/169).

Storage

Packs are kept by Tempa itself, in global tables (migration 0023), not in a tenant's config store: many tenants read one pack, so its rows cannot carry one reader's tenant id.

TableHolds
library_packsid, publisher (null: the installation; a tenant id: a partner, deleted with that tenant), name, description, visibility
library_pack_versionspack, version, the manifest (path → {blob, mode}) and its bundle id (the same content addressing as a config bundle, TEMPA-078: the same tree has the same id), the loaded pack.yaml, the changelog, requires, who published it and when, the yank
library_blobseach file's bytes once per pack (sha256, zlib), however many versions have it; per pack, so whoever may read a pack may read its files and nothing else

Who sees what is row-level security on those tables, forced like on every table:

  • library_packs is read by a session bound to a tenant when the pack is public, by its publisher, and by the clients of its publisher when it is partner_clients (the bound tenant's partner_id, TEMPA-168). A session without a tenant sees nothing.
  • Versions and blobs are visible exactly when their pack is.
  • Installation packs are written only in the system context (the operators' publish, yank and visibility change, reviewed call sites in tests/test_rls.py). A tenant may write only its own packs and never make one public (partner packs). Deletes happen only in the system context: a partner's packs go with the partner.

So the read API filters nothing itself: the database answers with what the caller's tenant may see.

Publishing

The installation's operators publish installation packs (auth.md: TEMPA_OPERATORS, with an Entra access token from the installation's directory; a device login is never an operator).

export TEMPA_OPERATOR_TOKEN=$(az account get-access-token --scope api://<tempa-api app id>/.default --query accessToken -o tsv)
tempa library publish ./packs/python-fastapi --endpoint https://tempa.devleaps.nl --forbid devleaps

or with the API: POST /v1/operator/library/packs with the pack as a zip or tar.gz body (pack.yaml at its root or in its one top folder; __MACOSX and .DS_Store are left out). The archive is read with the store's caps, links and paths outside the archive are refused. A refused pack answers 422 with {"detail": {"message", "issues": [{file, line, level, message}]}}.

EndpointWhoWhat
POST /v1/operator/library/packs?visibility=&forbid=operatorspublish a version (201; the same files again 200 with created: false); visibility public (new packs) or private; forbid words the pack must not contain (repeatable); audited library.publish
POST /v1/operator/library/packs/{id}/versions/{v}/yank {"reason"}operatorsyank a version, once; audited library.yank (MCP yank_library_version)
PATCH /v1/operator/library/packs/{id} {"visibility"}operatorspublic or private; audited library.visibility
GET /v1/operator/library/packsoperatorsevery pack, private and partners' ones too, with who published and yanked each version

The audit entries are in the operator's home tenant. A pack whose requires.tempa is newer than the installation is refused, as is a manifest whose publisher is not tempa.

Reading

Any signed-in user of a tenant (no role needed) reads the packs their tenant may see.

EndpointMCP toolWhat
GET /v1/library/packslist_library_packsthe packs with their versions (newest first: installable or yanked with the reason, changes), latest and contents (the latest version's asset names by kind)
GET /v1/library/packs/{id}get_library_pack(pack)one pack, latest_version in full: contents, default settings, requires, changelog
GET /v1/library/packs/{id}/versions/{v}get_library_pack(pack, version)one version (or latest) in full
GET /v1/library/packs/{id}/versions/{v}/filesget_library_pack(pack, version)its files: path, blob, mode, size; never the content
GET /v1/library/packs/{id}/versions/{v}/assetsget_library_pack(pack, version)its assets by kind (below) and default_mcp (TEMPA-121)
GET /v1/library/packs/{id}/versions/{v}/files/{path}read_library_file(pack, version, path)one file (UTF-8 as text/plain, else an attachment; the ETag is the blob hash)
GET /v1/library/packs/{id}/versions/{v}/file?path=read_library_file(pack, version, path)the same file as JSON (content, or binary: true; blob, mode, size), for tempa-web's previews (TEMPA-121)

Who published or yanked a version (an operator's address) is shown to operators only.

A version's assets are listed per kind (standards, skills, commands, agents, policies, mcp), each with its name, description (a Markdown asset's frontmatter; none for a policy bundle), path (the file that describes it: the Markdown, a skill's SKILL.md, a bundle's first Rego file, a template's YAML), all its files and their format (markdown, rego, yaml). An MCP template adds its url, auth, tools, its placeholders (name, description, example, required: every placeholder is in the URL, so a filled template needs them all) and the credential it needs, if any. default_mcp names the templates the pack's default settings use: adopting the defaults needs their values.

Subscriptions (TEMPA-118)

A tenant subscribes to a pack it may see on a node and pins a version. The config stays the one truth: the pack is put into the tenant's config tree, through a draft (config.md), and the same validator, resolver, preview, diff, rollback and Git flow work on it unchanged.

How a pack enters the config

packs/<pack id>/pack.yaml, standards/..., skills/..., policies/..., mcp/...   the pack version's files, verbatim
packs/<pack id>/pack.lock                                                     written by Tempa
nodes/.../node.yaml                                                           the node's settings add the pack's assets

pack.lock records the pack, version, publisher, the version's bundle id, the node, every file's sha256 and the values the subscriber gave for the pack's MCP templates:

# Written by Tempa (TEMPA-118): the library pack web-basics 1.0.0, subscribed on team-platform.
# Do not edit anything under this folder: a new version comes as an update proposal.
pack: web-basics
version: 1.0.0
publisher: tempa
bundle: sha256:…
node: team-platform
files: { pack.yaml: "sha256:…", standards/web-style.md: "sha256:…", … }
mcp_values: { status-page: { status_host: status.acme.example } }
  • The validator loads each packs/<id>/ as a pack: its standards, skills, commands, agents and policy bundles join the config's own, and an MCP template whose placeholders all have a value becomes an MCP endpoint with its URL filled in (one without its values is left out with a warning). The folder must hold exactly the files of its lock, with those hashes and that bundle id: a file changed, added or removed by hand is an error ("files under packs/ change only through a pack update"), in a draft and in a Git tenant's pull request alike. Drafts also refuse a put, delete or rename under packs/ (422, action: "subscription").
  • Asset names are one namespace. A pack's asset keeps its own name (renaming would break a skill's Agent Skills name and a bundle's Rego package, and the files would no longer be the pack's); a name the config or another subscribed pack has already is an error, and subscribing to such a pack is refused (409 with the clashes). To change a pack's asset, add your own under another name.
  • The node's settings add the pack's assets like any other (standards: {mode: extendable, add: [...]}, policies bundles, mcp for filled templates), in the node's own node.yaml (or tenant.yaml). Only the settings: block is rewritten, one line per key; the rest of the file stays as it is. The admin may still change a mode or drop an item there. With adopt_defaults the pack's default settings (pack.yaml) apply to the keys the node does not set yet (with their mode); permissions merge; a key the node sets stays the node's.
  • Why not a packs: setting the resolver expands: the resolver, the mode rules, the semantic diff and the node editors would all have to learn it, and settings would come from two places. With the assets in the node's settings the resolver needs nothing new, and the profile's why points at the node as for any asset.

Subscribe, unsubscribe, status

EndpointMCP toolWhoWhat
GET /v1/library/subscriptions?status=current|alllist_subscriptionsconfig.readthe subscriptions: node, pinned version, status, the subscribing or unsubscribing draft, the open proposal, latest, update_available, yanked and a warning; untracked: packs in the live config without a subscription
POST /v1/library/subscriptions {pack, node, version?: "latest", adopt_defaults?, proposals?: true, mcp_values?}subscribe_packconfig.edit (tenant node)a draft "Subscribe to <pack> <version> on <node>" (201: subscription, draft, and for a Git tenant git); audited library.subscribe
POST /v1/library/subscriptions/preview (the same body)preview_subscriptionconfig.edit (tenant node)what subscribing would change, without a draft (TEMPA-121): the node's settings before and after, changed keys, default settings adopted (keys the node did not set) and kept (the node's own stays), the assets added, skipped_mcp (templates without values) and the draft's files; refused like the subscription (404, 409, 422)
DELETE /v1/library/subscriptions/{id}unsubscribe_packconfig.edita pending subscription ends at once (its draft is discarded); an active one gets a draft "Unsubscribe from <pack>" that removes packs/<id>/ and the pack's assets from the node's settings (settings adopted from the defaults stay); audited library.unsubscribe
PATCH /v1/library/subscriptions/{id} {proposals}set_pack_proposalsconfig.editwhether newer versions come in as proposals
POST /v1/library/subscriptions/proposals {pack?}propose_pack_updatesconfig.editlook for newer versions now (below)
GET /v1/config/drafts/{id}/archiveget_draft_archiveconfig.reada draft's whole tree as a zip (MCP: its files with content)
  • Only a pack the tenant may see can be subscribed (the library's row-level security: public packs, its partner's partner_clients packs, never another tenant's private ones; 404 otherwise), and never a yanked version (409). One subscription per pack and tenant: packs/<id>/ holds one version. Another node can add the same assets to its own settings by hand.
  • library_subscriptions (migration 0026, a tenant table with forced row-level security) records the intent; its status follows the live config: pending until the subscribing draft (or, for a Git tenant, the commit of its files) is live, then active with the live lock's version as the pin; unsubscribing while the removing draft is open; ended once the pack is gone from the live config (also after a rollback) or the subscribing draft was discarded.
  • The drafts record their origin (config_drafts.origin, {"source": "pack", "kind": "subscribe" | "update" | "unsubscribe", "pack", "version"}) and a description (config_drafts.description: what it does and the changelog).
  • Git tenants (config_mode: git) get the same draft: publishing it is refused with the pull_request action (TEMPA-084; TEMPA-089 turns it into a pull request). The answer's git lists the paths to commit (changed, removed) and the draft's zip (archive; tempa-web: Download its tree as a zip on the draft page). Once the commit is live the subscription is active.

Update proposals

When a newer installable version of a subscribed pack is out, Tempa makes one open draft "Update <pack> to <version>" per subscription that wants proposals: the live tree with packs/<id>/ replaced by the new version (the MCP values carry over), the node's settings adding the version's new assets and dropping the removed ones, and the changelog since the pinned version in the description. Then the admin reviews (diff, preview) and publishes it; Git tenants commit it.

  • Never published by Tempa, never a second proposal while one is open (publish or discard it; the next run proposes the newest), never a yanked version and never a downgrade. Each subscription's row is locked while it is looked at, so several replicas make one proposal.
  • When: every TEMPA_LIBRARY_PROPOSAL_HOURS (6 by default; 0 turns the periodic run off) for every active tenant, right after an operator publishes a new version (for the tenants subscribed to that pack), and on propose_pack_updates. The job sees across tenants only to find the tenant ids with subscriptions (a reviewed system context); each tenant's proposals run in a session bound to that tenant, so they see only the packs that tenant may see. Audited library.propose (actor tempa:library, channel library).
  • A pinned version that was yanked gets a warning in the list ("web-basics 1.0.0 was yanked (reason): update to 1.1.0"); the proposal comes as for any newer version.

The library in tempa-web (TEMPA-121)

Library in tempa-web's navigation (every admin role; web.md) shows the packs the tenant may see as cards: name, description, publisher (Tempa for the installation, from <partner> for the partner that manages the tenant, Yours for the tenant's own, private when only its publisher sees it), the latest version, the kinds of assets inside and a subscribed badge with the pin and the node. Search looks in the name, the description and the asset names; filters narrow by publisher, kind and subscription.

A pack opens in a dialog: the tenant's subscription (status, pin, node, the draft to review or the open update proposal, Propose <version> now when a newer version is out and none is open, Unsubscribe), the versions newest first with their changes (a yanked version is marked with its reason; Show switches the contents to it), the contents per kind with a read-only preview of every file (Markdown rendered as elements with the asset editors' renderer, TEMPA-086, never as HTML; Rego and YAML as monospace) and the settings the pack suggests.

Add to a node (config editors of the whole tenant, as the API):

  1. Pick the node in the tree and the version: Latest (newer versions then come as update proposals) or an installable version to pin.
  2. Optionally take the pack's default settings (for the settings the node does not set yet).
  3. Fill in the MCP templates: one field per placeholder, with its description and example and the URL as it will be. A template left empty is left out (its endpoint is not added, the validator warns); once one field is given, every field is needed; a template the adopted defaults use is required. Values are host names or names (letters, digits and . _ ~ : -), checked as you type and again by tempa-api. A template that needs a credential names the tenant secret to store it in (mcp_credential:<name>).
  4. What changes shows the node's settings before and after, which defaults are adopted and which keys the node keeps, and the templates left out (POST /v1/library/subscriptions/preview, nothing is written).
  5. Make the draft creates the subscription's draft and opens it: review the diff and the preview, then publish it; a Git tenant downloads its zip and commits it.

The Config page's Library packs card stays the place to see every subscription at once and to look for updates.

Partner packs (TEMPA-169)

A partner (auth.md) publishes its own packs for the tenants it manages: its standards, skills and guardrails, pinned and updated in each client like any pack.

Publishing

The partner tenant's partner_admins (and platform owners) publish with their own login, no operator token:

tempa login --tenant devleaps                       # the partner tenant
tempa library publish --partner ./packs/devleaps-python [--visibility partner_clients|private] [--forbid acme]
EndpointMCP toolWhat
GET /v1/partner/library/packslist_partner_packsthe partner's own packs with every version (who published and yanked it) and its content settings
POST /v1/partner/library/packs?visibility=&forbid=publish_partner_pack (base64, 10 MiB)publish a version (zip or tar.gz, as for operators); 201, the same files again 200 created: false
POST /v1/partner/library/packs/{id}/versions/{v}/yank {"reason"}yank_partner_packyank a version of its own pack, once
PATCH /v1/partner/library/packs/{id} {"visibility"}set_partner_pack_visibilitypartner_clients or private
GET|PUT /v1/partner/library/settingsset_default_client_packsdefault packs for new clients, auto_publish_initial, forbid
GET /v1/partner/library/clients?client=list_client_pack_statusper client the partner's packs it runs (below)
  • pack.yaml names the partner tenant as publisher (refused otherwise, 422). The same validator as for installation packs, with the partner's forbid words (its settings) and the request's.
  • Never public. New packs are partner_clients (its clients see them), or private (the partner only, for trying a pack out). Public is refused by the API and by the library tables' row-level security (a tenant writes only its own packs, never a public one): the partner's requests write in a session bound to the partner tenant, without a system context.
  • Pack ids are one namespace on the installation: an id the installation or another partner holds is refused (409), whether the partner can see that pack or not.
  • Audited library.publish, library.yank, library.visibility in the partner tenant. A new version is proposed to the clients that subscribe right after the answer (propose_everywhere); a client sees only its own partner's packs, so other tenants get nothing.

Clients

A client sees its partner's partner_clients packs in the library and subscribes like to any pack. In the library and the subscriptions (GET /v1/library/packs, /v1/library/subscriptions) such a pack has from_partner: {id, name}, and tempa-web labels it "from <partner>". The client owns its config: every update is a proposal its admins publish or discard, it can switch proposals off and unsubscribe whenever it wants. A partner cannot force content on a client after the initial setup below; a pack the partner makes private or yanks stays in the clients that have it, with a warning.

Default packs for new clients

The partner's content settings (partner_content in its platform settings; only through /v1/partner/library/settings, never PATCH /v1/tenant/settings):

{
  "default_client_packs": [
    {"pack": "devleaps-python", "version": "latest", "node": "tenant", "adopt_defaults": false},
    {"pack": "devleaps-web", "version": "1.2.0", "node": "team-web", "adopt_defaults": true}
  ],
  "auto_publish_initial": false,
  "forbid": ["acme", "globex"]
}
  • A default is the partner's own partner_clients pack or a public installation pack, at an installable version (checked when saved). node: tenant (the client's tenant node) or a node key, which the initial setup creates as a team under the tenant node.
  • Creating a client (POST /v1/partner/clients, TEMPA-168) then makes one draft in the client: "Initial setup from <partner>: <packs>", origin {"source": "partner", "kind": "initial"}, with a minimal tenant.yaml, every default pack under packs/<id>/ and its assets in its node's settings, and a pending subscription per pack. One draft, not one per pack: a brand-new client has no config, and every draft of an empty tenant adds tenant.yaml, so a second draft could never be published after the first. A default the client may not see (made private, unknown), a yanked version or a pack whose asset names clash with an earlier one is left out and listed in initial_setup.skipped of the create answer. Adopted default settings leave out mcp (MCP templates need the client's values). While the draft is open, unsubscribing one of its packs is refused (409): publish it and unsubscribe after, or discard the draft to drop them all.
  • The auto-publish rule (decided). auto_publish_initial is off by default: the client's platform owner (or a partner consultant with a role in the client) reviews and publishes the initial setup. When the partner turns it on, Tempa publishes the initial setup at once, and only that: a brand-new client has no config and nobody's profile changes, and the partner set the client up on purpose. Nothing later is ever published by Tempa: new versions and later defaults are proposals, and new defaults never reach existing clients. A draft that does not validate (for example defaults that contradict each other) stays open, with initial_setup.publish_refused.
  • Audited partner.client_setup in the client and the partner, library.subscribe per pack in the client, partner.content_settings in the partner.

The partner's overview

GET /v1/partner/library/clients (tempa-web: Clients → Packs for your clients, and Your packs on a client's page) answers per client the partner's packs it subscribes to: pack, pinned version, status, the version of an open update proposal, the newest version and whether it is newer; and per pack how many clients subscribe. Facts only: not the node, not a draft, nothing of the client's own config or of other publishers' packs. It is read through a reviewed system-context helper (partners.client_pack_status) limited twice to the caller: the client's partner and the pack's publisher. The clients' subscriptions are reconciled with their live configs first (in a session bound to each client).

Default packs and proposals in tempa-web (TEMPA-281)

Clients → Packs for your clients edits the default packs (pack, version or latest, node, the pack's default settings) and Publish a new client's initial setup at once in place, saved together with PUT /v1/partner/library/settings (the forbidden words are kept as they are; audited partner.content_settings). Nothing reaches existing clients.

Propose to clients on one of the partner's packs proposes a version (default the newest installable one) to several clients at once: POST /v1/partner/library/packs/{pack}/proposals with {"clients": ["client-a", "client-b"], "version": "1.1.0"} (1 to 100 clients). Each client that subscribes gets one update proposal, the same draft the periodic job makes ("Update <pack> to <version>"), made by the partner admin in a session bound to that client; the client's admins publish it or not. The answer has a status per client, and one client failing never stops the others:

StatusMeaning
proposeda new draft is open in the client
draft_opena proposal waits in the client already (version is its version)
publishedthe client runs that version or a newer one
failedwith the reason: not a client of yours (also for another partner's client), not active, does not subscribe, its subscription is still pending or being removed, its admins turned proposals off for the pack, no live config

404 for a pack that is not the partner's own (a public installation pack included), 422 for a version that is not installable. Audited partner.pack_propose in the partner tenant (target <pack>@<version>, details.clients the status per client) and library.propose in each client that got a draft. A client that turned update proposals off is never sent one.

The overview across clients (TEMPA-281)

GET /v1/partner/overview (tempa-web: the Overview card on Clients; MCP get_partner_overview) answers one row per client of the partner (deleting ones left out):

  • status and plan ({id, name, assigned});
  • config: state valid (a live version), invalid (the newest version failed validation; the live one, if any, still serves; invalid_since) or none, the live_version and last_published_at;
  • adoption: developers, enrolled (a device that checked in within the stale window, the enrolment KPI) and active (a harness session in the last 7 days, from the telemetry store through a reader bound to the client; null with telemetry: not_configured or unavailable), with their shares of the developers. Counted per team, the KPI grain, and added up per client: no person, no team name;
  • packs: how many of the partner's packs it subscribes to, open update proposals, and newer versions out but not proposed;
  • attention: the reasons it needs the partner: suspended, no_live_config, config_invalid, low_enrolment (under half of its developers enrolled), low_active_use (under half of the enrolled ones active), update_not_proposed.

?needs_attention=true keeps the clients with a reason; totals counts all clients and the ones that need attention either way. Each client is read in a session bound to it after partners.clients (limited to the partner) named it, so another partner's clients never appear; the error text of an invalid version is not part of it.

Export

The partner's own packs (library_packs, library_pack_versions and library_blobs, the files' contents as zlib, base64) are in its tenant export under library; a client's export has none (its subscribed files are in its config store, exported with it). The packs are deleted with the partner.

The starter packs (TEMPA-119)

The installation's own packs live as source in this repo, library/packs/<pack id>/, taken from Devleaps' own config and stripped of everything that belongs to one organisation (Java/Spring is written fresh).

PackStandardsSkillsPolicy bundle
tempa-baselinebaseline-security, baseline-git-and-pr, baseline-testing, baseline-code-qualitybaseline-tdd-workflow, baseline-bugfix-workflow, baseline-pr-descriptionbaseline_guardrails
python-fastapipy-python, py-fastapi, py-postgrespy-fastapi-endpoint, py-alembic-migrationpy_uv
typescript-nextjsts-typescript, ts-nextjs, ts-react, ts-vitest
java-springjava-21, java-spring-boot, java-testing, java-build
terraformtf-modules, tf-state, tf-plan-and-applytf_guardrails
  • Names: asset names share one namespace with the tenant's own, so every asset carries its pack's prefix (baseline-, py-, ts-, java-, tf-; _ for bundles). A test keeps them unique between the packs and apart from a typical config and the bundles tempa-api ships.
  • Defaults: standards extendable, skills open, policy bundles locked. tempa-baseline also denies the file tools on credential files; it leaves allow_external_mcp at the product default (the free tier's baseline).
  • Policy bundles are self-contained (no import data.helpers), so they work next to any bundle a tenant has. baseline_guardrails is the lean, generalised universal: everyday read and workspace commands allowed with workspace-relative paths; credential files, environment dumps, secret store reads and printed tokens denied; sudo, disk and power commands, rm outside the workspace, piping into a shell, force pushes and deleting cloud resources denied; discarding work, rewriting history, pushing to main, inline code, wrappers, publishing and other cloud changes asked. Terraform is left to tf_guardrails: fmt, validate, init and plan allowed; apply, destroy, state changes and printing state asked; -auto-approve (also through TF_CLI_ARGS or Terragrunt's --non-interactive) denied. py_uv is the generalised python_uv.
  • Checks (CI, api/tests/test_library_packs.py and test_library_policies.py): every pack validates without warnings with the words in library/forbidden-words.txt forbidden, every bundle passes opa check --strict and its Rego unit tests in library/tests/<bundle>/, and the bundles are run through the policy engine on real command lines.
  • Changing a pack: bump version in its pack.yaml and add a changelog entry on top; a published version never changes. Subscribed tenants get the new version as an update proposal.

Publishing the starter packs (operators)

  1. Build and check the archives: make library-packs validates every pack with the forbid list and writes library/dist/<id>-<version>.tar.gz (reproducible: the same files give the same bytes).
  2. Get an operator token for the installation (an operator in TEMPA_OPERATORS):
    export TEMPA_OPERATOR_TOKEN=$(az account get-access-token --scope api://<tempa-api app id>/.default --query accessToken -o tsv)
  1. Publish each pack to dev first, private, with the same forbid list the server checks again:
    FORBID=$(paste -sd, library/forbidden-words.txt)
    for pack in tempa-baseline python-fastapi typescript-nextjs java-spring terraform; do
      tempa library publish library/packs/$pack --endpoint https://tempa.dev.devleaps.nl --visibility private --forbid "$FORBID"
    done

    (or POST /v1/operator/library/packs?visibility=private&forbid=... with an archive from step 1 as the body).

  2. Check on dev: GET /v1/operator/library/packs lists the five packs (1.0.0, tempa-baseline 1.1.0); subscribe a test node to each (POST /v1/library/subscriptions, adopt defaults), look at the draft's preview, then discard it.
  3. Make them visible to every tenant: PATCH /v1/operator/library/packs/{id} {"visibility": "public"} per pack.
  4. Repeat steps 3 to 5 against production (--endpoint https://tempa.devleaps.nl).

Publishing the same version with the same files again changes nothing, so a rerun is safe; a changed file under an already published version is refused (409): bump the version.

The free tier's baseline (TEMPA-120)

Every tenant an operator creates starts with tempa-baseline live, so a free tenant (every tenant until plans exist, TEMPA-071) is guarded from its first session without writing any config:

GuardrailWhere
Secrets in the shell: credential files (.env, keys, SSH and cloud credentials, Terraform state), environment dumps, secret store reads and printed tokens are deniedbaseline_guardrails (secrets.rego), locked
Secrets in the harness's own file tools: Read of the same credential files is denied (.env.example and other templates stay readable)the pack's default permissions (deny, extendable)
Destructive commands: sudo, disk and power commands, rm outside the workspace, piping into a shell, force pushes and deleting cloud resources denied; discarding work, rewriting history, pushing to main and cloud changes askedbaseline_guardrails (dangerous.rego, workspace.rego, git.rego, cloud.rego), locked
MCP allowlist, opt-in: Tempa's own endpoints always pass only as the config names them (and their tools allowlists); other MCP servers stay allowed under the harness's own permissions, as the product decided (#133), until the owner opts in (below)the policy engine's MCP allowlist (policies.md); the pack does not set allow_external_mcp

Allowing only the config's MCP endpoints is the tenant owner's choice, not a default. A platform owner sets this in tenant.yaml (in a draft, or in the config repo), as the pack's README.md says:

settings:
  allow_external_mcp: { mode: locked, value: false }

locked keeps every node below the tenant from switching it back (open lets a team decide). Every other MCP server's tools are then denied (tempa/mcp-allowlist).

How it gets there: POST /v1/operator/tenants (and the MCP create_tenant) makes the new tenant's initial setup the way a partner's new client gets its partner's default packs (above): one draft "Initial setup: tempa-baseline 1.1.0" (origin {"source": "installation", "kind": "initial"}) with a minimal tenant.yaml, the pack under packs/tempa-baseline/, its assets and its default settings adopted on the tenant node, and a subscription. Tempa publishes it at once: the tenant is brand new, so nobody's profile changes. The answer's initial_setup names the packs, the published version and any pack left out.

  • TEMPA_DEFAULT_TENANT_PACKS (default ["tempa-baseline"], [] for none) chooses the packs (operations.md). A pack the installation has not published (or only yanked versions of) is left out and named in initial_setup.skipped; the tenant is created anyway, bare. So publish the starter packs (below) before creating tenants.
  • A partner's client gets its partner's default packs instead, never the installation's.
  • From then on the baseline is the tenant's own config: newer versions of the pack come in as update proposals, and its platform owner can change the adopted settings in tenant.yaml (for example add allow_external_mcp as above) or unsubscribe.
  • A tenant that switches to a Git source (TEMPA-079) takes its config from its repository: commit packs/tempa-baseline/ and the settings from the draft's zip (GET /v1/config/drafts/{id}/archive) to keep the baseline.

What builds on this

  • TEMPA-120 the free tier's baseline: above.
  • TEMPA-121 the library in tempa-web: above.
  • TEMPA-089 turns a Git tenant's subscription, update and unsubscribe drafts into pull requests (the pull_request refusal and the archive are the hand-off today).
  • TEMPA-169 partners publish packs for their clients: Partner packs above.