Skip to main content
Version: v5.7.0

The pack store

Included from Basic tier

The pack store is the platform's registry of installed action packs: signed pack bundles uploaded whole through the admin surface — the air-gap lane: download the bundle, carry it in, upload it — and kept as immutable versioned units, the rows in the pack_store table, the bundle bytes in the blob store. A template mounts an installed pack with installed: <pack> in .opschain/packs.yaml instead of a git source (the user mechanics live in opschain docs getting-started/2-4-using-packs); the free bundled common pack never needs the store. Every procedure requires the /admin updatable determination (permission_denied on deny), and a server composed without the store answers not_found.

Mounting an installed pack from packs.yaml​

A repository references an uploaded bundle by its store name — an installed: entry in .opschain/packs.yaml in place of a git source:

packs:
- name: aws # the namespace the pack's actions mount under
installed: opschain-pack-aws # the pack's name in the platform's pack store
# version: 1.2.3 # optional: pin this exact installed version

With version: the declaration pins that exact installed version. Without it, the version is chosen when the template version resolves — the pack's admin pin when one is set, else the highest installed semantic version — and sealed into the template version's pinned set (the rules below). The name: mount point works exactly as for a git-mounted pack: the pack's actions appear as aws:*, and the declaration must name exactly one source — installed:, remote:/url:, bundle:, or none of them for the bundled common pack.

Inside the pack, an action file may live directly under .opschain/actions/ (actions/whoami.yaml) or under a directory named after the pack's own mount (actions/aws/whoami.yaml — the layout the maintained cloud packs ship). Both answer the same mounted reference, aws:whoami: resolution tries the direct path first, then the mount-named one, and the action catalog lists each action once under its mounted name whichever layout holds the file.

A mounted pack whose content cannot be read — an uninstalled version, a failed verification — never empties the catalog: the template's own actions keep listing, and the pack appears as a single entry named after its mount carrying the failure message. Creating a change that references the pack still refuses with the same named error.

The bundle, and what signs it​

A bundle is a gzip-compressed tar whose first entry is manifest.json — a signed envelope read before any content byte, so every later entry is checked against the signed file list. The manifest travels in the same canonical-JSON + Ed25519 envelope as a licence file and verifies against the same product keyring embedded in the binary; signing lives outside the product, with LimePoint, so a customer cannot mint one — and a registration-restricted key is refused by name even with a valid signature. The signed payload carries the pack's identity, its authoritative semantic version (the filename and any release tag are decoration), the packs:<name> entitlement it requires (empty = ships in every tier), the minimum platform version it runs on, and a per-file list of SHA-256 digest, exact size and executable bit — executability travels in the signed manifest because the container's own headers are untrusted decoration.

A cloud pack's release bundle is self-contained: it carries the pack's native tool as a tools/ directory at the pack root — one tarball per supported runner platform plus their SHA256SUMS — covered by the same per-file digests and manifest signature as every other file. Installing the bundle therefore equips runners end to end: the runner materialises the verified pack content, and the pack's ensure step installs the tool from tools/ after re-checking its pinned digest — no toolchain, no download configuration and no network egress on a fresh runner. One uploaded bundle is capped at 64 MiB.

Install verifies the whole bundle offline, before a byte is stored, and each failure refuses by name on the upload's content field:

  • unknown signing key: the bundle names a signing key this platform's keyring does not hold
  • bad signature: the bundle's manifest signature does not verify against the product keyring — editing any manifest byte breaks it
  • digest mismatch: … — a content file missing, unlisted, resized or altered relative to its signed digest
  • encrypted bundle: this platform version only installs plain bundles (the manifest's encryption field is a format seam; any declared scheme, or an age-encrypted stream, refuses rather than mis-reading ciphertext)
  • malformed bundle: … — not gzip+tar, manifest not first, an oversized manifest, an invalid path, a duplicate entry

Two further gates run after verification: a manifest requiring a packs:<name> entitlement the licence does not grant refuses install with the typed permission_denied (ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED, metadata naming the feature and tier), and a min_platform newer than this server is the failed_precondition platform too old: pack <pack> version <version> requires platform <min> or newer, and this server is <server> (a dev build whose own version does not parse never blocks).

Verification is not install-only: every read back out of the store — the compile and catalog lanes, the runner content archive — re-verifies the manifest signature and every per-file digest on the way out, so content tampered with at rest never serves.

Versions: immutable, co-existing, pinnable​

  • Versions are immutable. Re-uploading an installed version with identical content is idempotent (reported as already installed); different content refuses — this pack version is already installed with different content — versions are immutable, publish a new version.
  • Versions co-exist. Prior versions stay installed side by side, so different template versions pin different pack versions and run concurrently. Ordering is strict semver (a pre-release sorts below its release; build metadata is rejected outright — two artefacts distinguished only by build metadata could never co-exist honestly).
  • Resolution seals. An installed: declaration with version: pins that exact version. Without it, the version is chosen when the template version resolves — the pack's admin pin when one is set, else the highest installed semantic version — and sealed into the template version's pinned set, copied onto each run at change creation, so dispatch and the audit trail read one immutable set.
  • Pinning steers future resolutions only. PinPackVersion marks one version as the unversioned resolution target — the estate default; already-sealed template versions keep the exact version they sealed. Rollback is a re-pin, never a re-upload; clearing the pin restores highest-installed-semver resolution.
  • A template version may pin a pack version of its own. A template version pins its Git SHA, and it pins its pack versions the same way: for a pack its packs.yaml declares WITHOUT version:, an operator may choose the installed version that one template version seals — the explicit per-version pin (templates version upsert --pack <name>=<version>, the v2 UpsertTemplateVersion.pack_pins, the GUI's Sealed packs panel; --pack <name>= clears it back to the estate default). The choice is recorded on the pin with its provenance, decided_by = explicit, beside admin_pin / highest_installed (the estate default decided it) and declared (the declaration carries version: — the file seals it and no per-version pin can move it, ERROR_REASON_PACK_DECLARATION_SEALED). An unlocked version's pins may be managed this way; a locked version takes no change (unlock first — the lock is the seal).
  • Deletion is explicit and refuses while needed: this pack version is pinned — clear the pin before deleting it, or pack <name> version <version> is still referenced by <N> template version(s) and <M> run(s) — re-resolve those templates onto another version (opschain templates re-resolve); runs keep the content they executed until retention ages them out (each clause only when that count is non-zero). Sealed sets must keep resolving the exact content they pinned, and a run's evidence keeps its pack content for the run's lifetime.
  • References are visible before a delete, not only in its refusal. Every version a listing returns carries referenced_by — the template versions whose sealed set pins it (project, template code, version label and id, so the GUI links each one) — and run_count, the runs whose frozen set pins it; both come from the same pack-pin rows the delete refusal counts, filled in one query per listing page (and on the install and pin responses). The other direction sits on the template version as sealed_packs ({name, version, mount, decided_by} per installed pack mount). The two counts move exactly as the refusal does: a re-resolve cuts a new template version sealing the target, so the source version's reference stays with the old template version until that template version is deleted or archived away; a run's reference leaves only when retention removes the run. opschain admin packs list prints them as REFERENCED BY (<n> tv / <m> runs), -o json carries the full list.
  • Bulk upgrade is a verb, not a side effect. ReResolvePack finds every template whose latest version resolves the pack through an unversioned installed declaration and cuts one new template version per template with only that pack pin moved; explicit version: declarations are never touched, dry_run reports without writing, and queue_asset_upgrades moves each template's assets through the standard assignment flow under per-asset authorisation. CLI: opschain docs cli/templates-re-resolve.
  • A fetch of the revision re-resolves the pins but keeps the explicit ones. Refetching an unlocked template version resolves its packs.yaml at the new commit and replaces the sealed set — the estate default decides every unversioned mount again — except that a mount carrying an explicit per-version pin keeps it, re-validated as installed (a version no longer installed refuses the fetch by name: set another or clear the pin first). This is what stops a labelled version such as 1.0.0+0.2.5 silently sealing a different pack version than its label on its next fetch. Locked and archived versions are not refetched and their pins never change.
  • ReResolvePack with a version writes the explicit pin. Without a version the verb cuts onto the estate default and the cut versions carry that provenance (decided_by on the report: admin_pin or highest_installed); with one, the named version is written as every cut version's explicit pin (decided_by: explicit), so later fetches keep it — a dry run reports the same (will set explicit pin X on N templates in the CLI). A declaration carrying version: is still never touched (status pinned), and a latest version already sealing the target is up_to_date whatever its pins' provenance (set a version's own provenance on the version itself, never by cutting an identical one). The earlier refusal (ERROR_REASON_PACK_TEMPLATE_UNSEALED) is retired: the reason value stays for wire compatibility and is no longer emitted.

The entitlement gate​

A pack's signed manifest declares the entitlement it requires, and the gate reads that declaration — never a hard-coded pack list — at two doors: the install refusal above, and every run-creation door that uses the pack (the git-remote lane, the templated lane including workflow-nested changes, and the retry lane, which re-gates on the original run's frozen set, so an entitlement that lapsed since the original creation refuses the retry). The run-creation refusal is the named 422 quoted on entitlements — pack, entitlement and remedy. Dispatch of an already-created run is deliberately not re-gated (the in-flight carve-out). A pack with no manifest, or one whose manifest does not verify, is never gated — the licence system only guards LimePoint-shipped value, so unsigned customer and community packs always run; a bad signature on a git-sourced pack's manifest is the loud warn:packs:unverified_manifest event, never a refusal. (The local pack cache, opschain packs fetch, is the one stricter door: a manifest that does not verify is refused and the pack is never cached — a local fetch is a deliberate act, see opschain docs cli/packs-fetch.)

Events​

All writes are audited as ordinary triggerable events (opschain docs events): api:packs:install (name, version, requirement, signing key, bundle digest, uploaded filename), api:packs:pin, api:packs:destroy and api:packs:re_resolve (plus one info:template_version:pack_re_resolved per cut version). Verification refusals are the caller's named error and store nothing.

The local bundle lane​

The same signed .bundle artefact mounts straight from disk in a --local run (bundle: <path> in .opschain/packs.yaml), verified identically — manifest signature, every digest, any required entitlement — with no store and no pin: the file on disk is the version. A server never reads local disk, so on a server the same entry is a named refusal pointing at installed:. See opschain docs getting-started/2-4-using-packs.

SurfaceWhat
PackStoreService (API v2)InstallPack, ListPackVersions, PinPackVersion, DeletePackVersion, ReResolvePack — see opschain docs api-v2.
opschain admin packsThe CLI family: opschain docs cli/admin-packs, with cli/admin-packs-install, cli/admin-packs-list, cli/admin-packs-pin, cli/admin-packs-delete.
opschain templates re-resolveThe bulk upgrade verb: opschain docs cli/templates-re-resolve.
entitlementsThe packs:<name> entitlements bundles may require, and the named refusals.