Reusable GitHub workflows + actions for Liskov CI, so each Liskov repo's CI is a
short uses: …@v2 instead of copy-pasted YAML + scripts. Plan + decisions:
liskov-agent-orchestrator BKLG-20260624-jn9l (extracted from liskov-diagnostic's
inline scripts).
The Liskov upload default described here is pending the next v2.x.0 release
and movement of v2; this source change does not cut a release.
# .github/workflows/<app>.yml in an Acurast app repo
on:
push: { branches: [main], paths: ['<app-dir>/**'] }
workflow_dispatch:
permissions: { id-token: write, contents: read }
jobs:
artifact:
uses: proof-computer/liskov-github-actions/.github/workflows/acurast-app.yml@v2
with:
organization-id: your-liskov-organization-id # ordinary non-secret configuration
app-id: uptime-prober
working-directory: uptime-prober # default "."
entrypoint: app.cjs # default; use bundle.cjs + extra-files: app.cjs for a stage0 app
authored-manifest-path: uptime-prober/.liskov/uptime-prober.policy.jsonRuns pnpm install --frozen-lockfile → typecheck → test → build (optional smoke),
pins the Acurast deploy zip to IPFS no-spend, and posts a GitHub-OIDC artifact
pin. V4 targets remain bound to the exact authored and release-intent digests;
identity-only V5 source targets are fenced by the server-owned source binding and
never synthesize V4 digests or drafts. Callers keep
the generated-zip inputs with app-id, authored-manifest-path,
working-directory, entrypoint, extra-files, node-version, smoke, attest,
and pin-url. The workflow exposes the CID, digest, uploaded build-manifest path,
artifact-version IDs, per-Application result JSON, and target count.
With ipfs-endpoint empty, ipfs-pin uses GitHub OIDC (audience
liskov-artifact-upload, requiring permissions: id-token: write) to request a
single-use token from
<liskov-url>/api/organizations/<organization-id>/applications/<app-id>/artifact-upload-intents.
Set organization-id to the existing application's organization; it selects
the resource and grants no permission. The default liskov-url is
https://api.liskov.proof.computer; pin-url remains the separate attestation
override. The upload uses the returned token and verifies exact bytes through
PROOF's returned gateway template. The authored manifest path is repository-relative;
its digest uses the same canonical policy contract as attestation. When that path
is empty, the intent's manifest digest is the sha256 of the artifact bytes.
To retain the Acurast proxy path, commit its endpoint as the ipfs-endpoint
input (it is not a secret) and pass its Bearer key, if it needs one, as the
LISKOV_IPFS_API_KEY secret. A Liskov refusal fails the action with its code;
it never falls back to the proxy. This includes 409 source_binding_revoked.
The action adds upload-id (empty for proxy/reused paths) and upload-path
(liskov, proxy, or reused) outputs; the token is masked and never output.
Callers that already prepare deploy bytes may set prepare-command and
artifact-path. The path is relative to working-directory; ipfs-pin hashes and
uploads those exact bytes without repacking them. artifact-metadata-path accepts a
strict JSON sidecar containing only the currently supported Diagnostic evidence:
optional devtools (enabled, injected, and uploadMode: "direct"), artifact
(format, safe entrypoint, and restartPolicy), sourceBundleSha256, failure,
and scenarioArtifact. Unknown fields and unsafe paths fail closed. OIDC claims,
repository identity, refs, and workflow identity never come from this sidecar.
script-ipfs reuses an exact ipfs://CID without another upload while still hashing
the local bytes. Set ipfs-gateway-url to a credential-free HTTPS URL (with optional
{cid}) to override Liskov's gateway or opt into proxy/reused byte verification.
Verification allows four propagation
attempts with ten seconds between attempts. The build manifest is always uploaded as
a GitHub Actions run artifact.
For one prepared artifact shared by multiple Applications, set
artifact-targets-path instead of authored-manifest-path. It is a repository-root
relative non-empty JSON array with no extra keys:
[
{"applicationId":"diagnostic-a","authoredManifestPath":".liskov/diagnostic-a.json"},
{"applicationId":"diagnostic-b","authoredManifestPath":".liskov/diagnostic-b.json"}
]Each exact target must name the matching Application and be either a V4 unencrypted
ipfs_bundle build manifest or a retained V5 JavaScript source document. The shared
action mints one OIDC token and posts one artifact per target. V4 requests retain the
deployed domain and byte shape; V5 requests use
proof.liskov.github-source-artifact.v1 with manifest path, CID, and digest only, so
Liskov resolves repository/ref/workflow/commit authority from verified OIDC plus the
current source binding. The original single-target inputs and first
artifact-version-id output remain compatible.
on: { push: { branches: [main] } }
permissions: { id-token: write, contents: read }
jobs:
publish:
uses: proof-computer/liskov-github-actions/.github/workflows/marketplace-ingest.yml@v2Mints an OIDC token and tells liskov-rs to reconcile the catalog to this commit. The
/api/marketplace/ingest endpoint is not built yet (catalog schema spec, Part 2) —
this is ready for it.
policy-sync.yml and actions/policy-import were removed in v2.3.0, when
Liskov closed V4 manifest import. Create the Application with
proof liskov application create, bind its repository with
proof liskov application source-binding set, and publish a V5 manifest with
proof liskov application policy publish.
on:
workflow_dispatch:
inputs:
image_url: { description: Pinned PRoot image URL, required: true, type: string }
expected_sha256: { description: Optional SHA-256, required: false, type: string }
permissions: { id-token: write, contents: read }
jobs:
runtime-image:
uses: proof-computer/liskov-github-actions/.github/workflows/runtime-image.yml@v2
with:
application-id: my-app
manifest-path: .liskov/my-app.v5.json # V5 source document
image-url: ${{ inputs.image_url }}
expected-sha256: ${{ inputs.expected_sha256 }}
attestation-repository: proof-computer/liskov-runtime-images
attestation-source-digest: ${{ inputs.image_source_commit }}
attestation-signer-workflow: proof-computer/liskov-runtime-images/.github/workflows/ci.ymlDownloads the supplied image and locally verifies its SHA-256 before any
upload. When attestation-repository, attestation-source-digest, and
attestation-signer-workflow are supplied, the workflow requires
expected-sha256 and verifies the downloaded file's GitHub artifact attestation
against that exact source commit and signer workflow before any state-changing
step. The three attestation inputs are all-or-none. Omitting all three preserves
the existing unattested direct-URL migration/debug path.
manifest-path is a repo-relative V5 source document (release.mode: source,
runtime.kind: native_image) whose applicationId is application-id. The workflow
imports nothing. The scoped upload action validates the document with the bundled
policy client and opens the upload session with the document itself and its path.
Liskov fences the verified GitHub OIDC identity against the Application's current
source binding before creating one-object Tigris credentials, and the action
requires the session to echo the authored digest it computed and the manifest path
before any byte is uploaded. It then uploads without exposing the credentials,
remints OIDC, finalizes, and returns the image, bootstrap, artifact-version,
cleanup, and safe provenance outputs. Set liskov-url to use a custom Liskov base.
Finalizing publishes nothing (auto-published is false). The workflow returns
the build evidence a V5 release.mode: source publication supplies:
| Output | Publish flag |
|---|---|
artifact-digest (the bootstrap bundle digest) |
--artifact-digest |
binding-revision |
--binding-revision |
revocation-epoch |
--revocation-epoch |
source-ref |
--source-ref |
source-commit |
--source-commit |
source-workflow-identity |
--workflow-identity |
source-repository |
(the repository the binding verified) |
Publish the same document with that evidence:
proof liskov application policy publish <app> --file <document> \
--artifact-digest <artifact-digest> \
--binding-revision <binding-revision> --revocation-epoch <revocation-epoch> \
--source-ref <source-ref> --source-commit <source-commit> \
--workflow-identity <source-workflow-identity> \
--expected-pointer-version <n> --yesor publish a release.mode: pinned document that names artifact-digest.
The Application must already carry a source binding for this repository, ref,
workflow and manifest path (proof liskov application source-binding set). This
V5 path needs a Liskov server that accepts the
proof.liskov.runtime-image-source-upload-session.v1 domain. @v1 carries the
retired V4 import path (policy-import, then an upload bound to the imported
digest pair), which Liskov refuses; a caller moves to @v2 with a V5 document.
This workflow is the reusable supply-chain path for native Cargo applications.
It requires Cargo.lock, an exact rust-toolchain.toml channel, and an
attested, digest-pinned helperless Liskov rootfs. On a native ARM64 runner it
builds the selected binary twice for aarch64-unknown-linux-musl, rejects a
dynamic interpreter, and requires identical binary bytes. It then overlays
each build at the declared absolute path, normalizes the rootfs archive, rejects
any embedded liskov-runtime-contact, and requires both complete images to
have the same digest and bytes.
Only after that proof does the workflow optionally attest the generated image
and invoke the scoped upload/finalization action with the V5 source document
at manifest-path (release.mode: source, runtime.kind: native_image). It
imports nothing, and returns the same publication evidence and takes the same
publish command as runtime-image.yml:
artifact-digest, source-repository, source-ref, source-commit,
source-workflow-identity, binding-revision and revocation-epoch. @v1
carries the retired V4 import path. Generated-image attestation defaults on. Set
attest-runtime-image: false when GitHub artifact attestations are unavailable,
including private repositories whose organization plan does not provide them.
This skips only publication of GitHub build provenance for the derived image:
Liskov still verifies the caller's GitHub OIDC identity and binds the exact
Application, source document, source commit, workflow, and image digest.
The base image remains digest- and attestation-verified in both modes.
GitHub requires the caller to grant every permission declared by a reusable
workflow, so callers must retain attestations: write even when this input is
false; the skipped step creates no attestation.
Callers must pin the base image's SHA-256, source commit, repository, and signer workflow. Release callers should use the maintained promoted rootfs; a release-candidate rootfs remains suitable only for an explicitly controlled canary.
permissions:
attestations: write
contents: read
id-token: write
jobs:
image:
uses: proof-computer/liskov-github-actions/.github/workflows/cargo-runtime-image.yml@v2
with:
application-id: rust-hello-world
manifest-path: .liskov/rust-hello-world.v5.json # V5 source document
working-directory: rust-hello-world
binary-name: rust-hello-world
install-path: /usr/local/bin/rust-hello-world
base-image-url: <immutable release asset URL>
base-image-sha256: <64 lowercase hexadecimal characters>
base-attestation-repository: proof-computer/liskov-runtime-images
base-attestation-source-digest: <exact source commit>
base-attestation-signer-workflow: proof-computer/liskov-runtime-images/.github/workflows/ci.yml
attest-runtime-image: false # optional; defaults to true
source-assurance: true # optional; defaults to falseWith source-assurance: true, the workflow regenerates and verifies the
committed sbom.cdx.json in working-directory before building. It exposes
source-digest (git-tree:<sha> of that directory at HEAD) and sbom-path
(sbom.cdx.json, relative to that directory) for the Marketplace descriptor's
sourceAssurance.sourceDigest and sourceAssurance.sbomRef. Both outputs are
empty when the input is omitted or false.
Generate the SBOM locally with Python 3.11 or newer and commit it alongside the source before enabling assurance:
python3 <actions-checkout>/actions/source-assurance/sbom.py --directory <app-dir>The generator uses pnpm-lock.yaml when present, otherwise Cargo.lock.
Cargo registry crates carry pkg:cargo/<name>@<version> package URLs and
their lockfile SHA-256 checksums; path and git crates carry no hash. Output is
deterministic, and existing pnpm output is byte-for-byte unchanged. The
generator records only lockfile facts; it does not infer dependency licences
or replace the separate third-party licence inventory required by ADR-0106.
Compose your own job from these (uses: proof-computer/liskov-github-actions/actions/<name>@v2):
| Action | Kind | Does |
|---|---|---|
setup |
composite | pnpm + Node + install --frozen-lockfile |
acurast-build |
composite | typecheck → test → build (+ optional smoke) |
ipfs-pin |
JS | generate/read exact bytes → Liskov OIDC upload + PROOF gateway verification (explicit ipfs-endpoint selects proxy), or reuse CID → cid/digest/manifest-path/upload-id/upload-path |
artifact-pin-attest |
JS | OIDC → one or more manifest-bound POST /api/applications/<id>/artifact-pins/github calls |
marketplace-ingest |
JS | OIDC → POST /api/marketplace/ingest |
runtime-image-upload |
JS | Hash → scoped Tigris upload bound to a V5 source document (manifest-path) → fresh-OIDC finalize → publication evidence outputs |
cargo-runtime-image-build |
composite + Python | Safely overlay one static AArch64 binary and emit a normalized helperless-rootfs-derived tar.xz |
source-assurance |
composite + Python | Verify a committed pnpm or Cargo CycloneDX SBOM → relative sbom-path and git-tree source-digest |
- Tag releases
vX.Y.Z;release.ymlmoves thevXmajor tag so consumers pin@v2and get the latestv2.x. Security-sensitive callers can pin a commit SHA. v1.0.1adds optional source-commit and signer-workflow verification for runtime images without changing existing direct-URL callers.v1.0.2adds the default-standard runtime-imagebootstrap-modeinput; thebridge-probevalue remains restricted by Liskov to its exact internal canary workflow.v1.2.0adds backward-compatible caller-prepared artifact bytes, sanitized Diagnostic metadata, existing-CID gateway verification, multi-target artifact-pin attestation, reusable-workflow outputs, and durable build-manifest run artifacts.v1.2.3allows Cargo callers to skip publishing derived-image GitHub build provenance while retaining deterministic byte proof, base-image attestation, and Liskov's exact OIDC/manifest/image binding.v1.2.4lets one attested IPFS bundle target both existing V4 build manifests and identity-only V5 source documents without creating a V4 compatibility draft.v2.0.0changes the reusable workflow's secret contract, so it is a new major.acurast-app.ymltakes its IPFS endpoint as theipfs-endpointinput and declares exactly two secrets,LISKOV_CODE_ENCRYPTION_KEYandLISKOV_IPFS_API_KEY.ipfs-pinreads its key fromLISKOV_IPFS_API_KEYand its endpoint only fromipfs-endpoint;artifact-pin-attestreadsLISKOV_ARTIFACT_PIN_URLbehind itspin-urlinput.v1is frozen atv1.3.2; a caller moves to@v2and its new names in one change.v2.3.0removes V4 manifest import:policy-sync.yml,actions/policy-importand the V4 mode ofruntime-image-upload. No@v2caller used them.- Reusable workflows reference their own JS actions by the literal
@v2major tag, so a caller pinned to@v2executes the matching released action surface.
Only no-spend (IPFS pin), OIDC attest/ingest, and scoped runtime-image upload
operations are exposed; the spend-capable ACURAST_MNEMONIC CLI-upload path is
intentionally not ported. Runtime-image sessions require the exact V5 source
document under the Application's current source binding, and upload credentials are masked, retained only in memory,
scoped to one object, and never emitted as outputs. The attest/ingest/upload endpoints
are gated server-side on repository, ref, workflow, and manifest authority.
pnpm install
pnpm typecheck
pnpm build # bundles each JS action src/ -> dist/index.cjs (committed)
pnpm check:dist # build + fail if committed dist drifted (CI runs this)actions/*/dist/index.cjs is committed (the node24 Actions runtime has no deps
installed). Edit src/, run pnpm build, commit both.
encryption-mode: aes-256-gcm-payload-v1 packages a public bootstrap and an
AES-256-GCM encrypted, self-contained CommonJS payload. The payload exports
async start(runtime) and uses the handle supplied by the bootstrap. It must
not start another runtime. The default none path is unchanged. The historical
V4 aes256_gcm whole-bundle requirement remains a different, unsupported format.
jobs:
artifact:
uses: proof-computer/liskov-github-actions/.github/workflows/acurast-app.yml@v2
with:
app-id: encrypted-worker
authored-manifest-path: .liskov/encrypted-worker.json
entrypoint: app.cjs
encryption-mode: aes-256-gcm-payload-v1
encryption-secret-id: application-code-key
secrets:
LISKOV_CODE_ENCRYPTION_KEY: ${{ secrets.LISKOV_CODE_ENCRYPTION_KEY }}This path requires a V5 release.mode: source, Node.js, the same authored
entrypoint file, and a required configuration.secrets declaration for
application-code-key with destination
{ "kind": "environment", "name": "LISKOV_CODE_KEY" }. Supply canonical standard
base64 encoding of a random 32-byte key in the GitHub secret, and install that
same value through the existing managed Lockbox secret path. The workflow does
not generate, upload or rotate customer secrets, publish policy, or authorize
spend. It refuses extra files, prepared archives and reused CIDs in encrypted
mode, so those inputs cannot quietly publish plaintext or reuse a nonce.
The runtime needs a processor with a working P-256 grant-response key. The
Android implementation requires Android 12 or later, and a DataEncryption
advertisement alone does not establish support. Select compatible candidates
in the Application manifest. Follow the encrypted JavaScript guide
to publish paused, confirm the managed key save, then resume within the reviewed
spend cap and verify signed loader and application outcomes.
Actions v2.1.0 embeds runtime SDK v0.3.35. Actions 1.3.2 embeds runtime SDK
0.3.30, including fresh-home creation, CommonJS module loading and bounded
failure-phase diagnostics. The public
bootstrap locates its runtime home inside the job bundle directory, where
Acurast grants filesystem access; it does not depend on device HOME or /tmp.
The action embeds the immutable runtime SDK commit recorded in package.json
and commits the built public loader beside its action bundle. Only the loader,
public descriptor, ciphertext and Acurast manifest enter the ZIP. OIDC evidence
binds the ZIP digest and encryptedCode metadata; payload digests are separate
from the ZIP digest. The runtime requires the key to have been installed by an
authenticated Lockbox grant and verifies both digests and the GCM tag before
loading a local file. The fixed AESGCM vector is shared with Rust and the SDK.
The released path completed production acceptance on 6 September 2026: job
160393 verified the managed grant, loaded the encrypted payload and emitted
signed application completion. The public guide and capability matrix retain
the separate registered V5 customer-release gate. Cargo image confidentiality,
PROOF operator blindness and zero-knowledge custody are not established by this
loader. A private repository alone does not protect ordinary none artifacts.
This repository is licensed under the Functional Source License, Version 1.1,
with Apache-2.0 as the future license (SPDX FSL-1.1-Apache-2.0); see
LICENSE. The licensor is Moose Labs Ltd. Each version becomes
available under Apache-2.0 two years after it is made available.
You may use, copy, modify and redistribute these actions and reusable
workflows for any purpose other than a Competing Use as LICENSE defines it.
Using them from any customer workflow, including by
uses: proof-computer/liskov-github-actions/..., to build, test and publish
your own work is not a Competing Use.
The change is forward-only: release tags cut before this license was added keep the terms they shipped with.