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 holdbad signature: the bundle's manifest signature does not verify against the product keyring— editing any manifest byte breaks itdigest mismatch: …— a content file missing, unlisted, resized or altered relative to its signed digestencrypted 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 withversion: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.
PinPackVersionmarks one version as the unversioned resolution target; 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. - Deletion is explicit and refuses while needed:
this pack version is pinned — clear the pin before deleting it, orpack <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) — andrun_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 assealed_packs({name, version}per installed pack). 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 listprints them asREFERENCED BY(<n> tv / <m> runs),-o jsoncarries the full list. - Bulk upgrade is a verb, not a side effect.
ReResolvePackfinds 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; explicitversion:declarations are never touched,dry_runreports without writing, andqueue_asset_upgradesmoves each template's assets through the standard assignment flow under per-asset authorisation. CLI:opschain docs cli/templates-re-resolve.
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.
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.
Related surfaces
| Surface | What |
|---|---|
PackStoreService (API v2) | InstallPack, ListPackVersions, PinPackVersion, DeletePackVersion, ReResolvePack — see opschain docs api-v2. |
opschain admin packs | The 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-resolve | The bulk upgrade verb: opschain docs cli/templates-re-resolve. |
| entitlements | The packs:<name> entitlements bundles may require, and the named refusals. |