Author a retained Application Manifest V5
This guide covers RC
sha256:549272988045e9357c4945850706569ed8dc7f0c6f419b7cf5c57d54b294bb10.
Capabilities and limits owns the supported
surface. Existing Application Manifest V4 Applications
keep running until they are retired; new publication is V5.
The retained V5 release candidate is deliberately narrow. It supports source or pinned releases, JavaScript or a curated native image, once/continuous/interval execution, at most two jobs on the first public surface, capability minimums, lifecycle timing, managed Service Credit spend, variables, secrets, logging, and Liskov-managed Runtime SSH.
V5 does not contain public ingress, provider integrations, cohort membership,
lifecycle hooks, durable state beyond off, placement diversity rules,
non-managed SSH providers, or self-custody spend. Those concepts require a
future policy version; adding their old draft spellings makes V5 fail closed.
For the complete first-use route — CLI installation, copying the starter into your repository, publication, and verification — follow Deploy from GitHub. This guide explains the document itself.
1. Build the checked once-mode starter
The checked
retained V5 starter fixture
is a complete repository root: source, tests, package and lock file, TypeScript
configuration, manifest, and reusable-workflow caller. It makes one GET
request to https://example.com/ and records only the host, success flag, and
HTTP status through managed logs. It sends no customer data and needs no
variable or secret.
Its entrypoint uses @proof-computer/liskov-runtime v0.3.33:
import {bootstrapLiskovRuntime} from '@proof-computer/liskov-runtime';
import {checkExampleDotCom} from './check.js';
async function main(): Promise<void> {
const runtime = await bootstrapLiskovRuntime({
component: 'hello-liskov',
logging: {mode: 'required'},
secrets: {mode: 'background'},
});
try {
await waitUntilReady(runtime);
const result = await checkExampleDotCom();
await runtime.log('starter.fetch.completed', result, {
severity: 'info',
labels: {component: 'hello-liskov'},
});
} catch (error) {
await runtime.diagnostics.fatal({
kind: 'explicit',
code: 'starter_fetch_failed',
component: 'hello-liskov',
error,
});
throw error;
} finally {
await runtime.flush();
runtime.stop();
}
}
// Managed log configuration can arrive after bootstrap. This check is bounded
// and never sends customer data before the runtime is ready.
async function waitUntilReady(runtime: {whenReady(): Promise<unknown>}): Promise<void> {
const deadline = Date.now() + 40_000;
for (;;) {
try {
await runtime.whenReady();
return;
} catch (error) {
if (Date.now() >= deadline) throw error;
await new Promise(resolve => setTimeout(resolve, 1_000));
}
}
}
void main().catch(() => { process.exitCode = 1; });
Commit this JSON document as .liskov/application-manifest.json:
{
"schema": "proof.liskov.application-manifest",
"schemaVersion": 5,
"applicationId": "hello-liskov",
"metadata": {
"description": "Fetch example.com and record the HTTP result in managed logs."
},
"release": {
"mode": "source"
},
"runtime": {
"kind": "javascript",
"engine": "nodejs",
"entrypoint": {
"file": "bundle.js"
}
},
"execution": {
"mode": "once"
},
"deployment": {
"schedule": {
"duration": "60s"
},
"spend": {
"unit": "service_credit_micros",
"perJob": "50000"
}
},
"state": {
"mode": "off"
},
"observability": {
"logs": {
"enabled": true
}
}
}
state.mode: off is required. It makes the absence of Liskov-managed durable
state explicit. Unknown fields, duplicate JSON/YAML keys, YAML anchors, aliases,
merge keys, tags, and multiple YAML documents are rejected.
From a clean checkout, install the exact lock, test, and build before creating or publishing anything:
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build
test -s dist/bundle.js
The build emits a CommonJS artifact and runs a compile-only smoke check. Acurast
loads the entrypoint with require(), so asynchronous startup stays inside
main instead of using top-level await.
Then validate the manifest locally:
proof liskov application manifest validate \
--file .liskov/application-manifest.json \
--json
Use @proof-computer/proof-cli-liskov 0.14.0. Its released tag contains
source commit e135604ed2f6c59ffc737fce5fe08eaa19d77d0c; the earlier v0.7.0 tag does
not. The application source-binding verbs in step 2 are included.
The 60-second schedule is deliberate. The Manifest V5 schema can parse shorter durations, but the Acurast marketplace refuses a job registration below its 60-second provider minimum. Local schema validation alone does not prove that a schedule can launch.
2. Create the Application and bind its exact GitHub source
For release.mode: source, the repository, ref, workflow identity, manifest
path, and artifact digest come from verified GitHub evidence. They do not
belong in the manifest: they are bound to the Application once, and every
build attests them.
Create the Application from identity alone, naming the repository that holds the document. Creation writes no draft and spends nothing:
proof liskov application create hello-liskov \
--repository OWNER/REPO \
--json
Bind the exact source the Application may publish from. This requires an
organization admin; a maintainer cannot retarget source. The first binding
is revision 1; a later change must name the revision it expects, and is
refused if another change landed first:
proof liskov application source-binding set hello-liskov \
--repository OWNER/REPO \
--allowed-ref refs/heads/main \
--workflow-identity OWNER/REPO/.github/workflows/liskov.yml@refs/heads/main \
--manifest-path .liskov/application-manifest.json \
--reason "first binding" \
--yes --json
application source-binding show hello-liskov --json reads the binding, its
revision, and its revocation epoch; application source-binding revoke
withdraws it. A publication whose ref is outside allowedRefs, whose workflow
identity differs, or whose manifest path differs is refused.
Run Application creation and source binding before the first push to main
that contains this workflow. The workflow's first dependent build must see the
existing binding; a successful local build does not create one.
Then let the reusable workflow build, pin, and attest the document on every
push to the bound ref. Its app-id, repository, workflow path, and
authored-manifest-path must be the bound values. The moving v2 tag
contains the retained V5 source binding, first released in v1.2.4
(aa1b83f0fd4b08ac33a6c9970d2077885922d79c):
name: Build Liskov Application
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
artifact:
uses: proof-computer/liskov-github-actions/.github/workflows/acurast-app.yml@v2
with:
organization-id: YOUR_ORGANIZATION_ID
app-id: hello-liskov
working-directory: .
entrypoint: bundle.js
authored-manifest-path: .liskov/application-manifest.json
Replace YOUR_ORGANIZATION_ID with the ID proof liskov whoami prints. The
workflow uploads the bundle through the Liskov IPFS service without spending and
attests the artifact digest, the source commit and ref, and the workflow
identity to Liskov. It publishes nothing. Its run log prints the artifact
digest as Artifact sha256:, and the called workflow exposes it as the
digest output.
3. Publish from the attested build
Publish the exact document the run attested. Every value must match what was bound and attested, or the publication is refused before anything is spent:
proof liskov application policy publish hello-liskov \
--file .liskov/application-manifest.json \
--artifact-digest sha256:ARTIFACT_DIGEST_FROM_THE_RUN \
--source-commit COMMIT_THE_RUN_ATTESTED \
--source-ref refs/heads/main \
--workflow-identity OWNER/REPO/.github/workflows/liskov.yml@refs/heads/main \
--binding-revision 1 \
--revocation-epoch 0 \
--expected-pointer-version 0 \
--yes --json
proof liskov application policy explain hello-liskov --json
proof liskov application status hello-liskov --json
Publishing is the mutation: it commits an immutable effective policy and
begins a spend-bearing deployment under the document's spend. Nothing is
sent without --yes, and the local document is validated first.
--expected-pointer-version is the active policy pointer you observed, 0
for a first publication; a stale value is refused rather than overwriting a
concurrent publication. --binding-revision and --revocation-epoch are the
values source-binding show reports.
Read the canonical explanation rather than recomputing policy client-side. Its
publication, execution, spend-closeout, and managed-SSH sections report
absent, notApplicable, refused, or satisfied with server-owned next
actions.
Execution and spend
Choose one execution arm:
{"mode": "once"}
A once Application runs one job and settles. It does not run again on its own.
To run the same document again today, create a new Application, which counts
against your organization's job slots. An explicit re-arm of the same
Application is release-gated, and
Capabilities and limits owns its current
boundary.
{"mode": "continuous", "until": "2027-01-01T00:00:00Z"}
{"mode": "interval", "every": "6h", "until": "2027-01-01T00:00:00Z"}
until is an optional bound. Durations are one integer plus ms, s, m,
h, or d; compound and unitless values fail.
Every deployment requires schedule.duration and spend. Recurring execution
also requires a rate:
{
"unit": "service_credit_micros",
"perJob": "600000",
"rate": {
"amount": "15000000",
"window": "1d"
}
}
Amounts are non-negative decimal strings, never JSON numbers. Thin V5 supports
only managed-custody service_credit_micros.
One or two jobs
deployment.jobs defaults to 1. The retained contract can parse 1–256, but
the first public capability and entitlement limit is exactly 2. Three or
more fails before spend.
{
"jobs": 2,
"schedule": {
"duration": "1h",
"phasing": {
"mode": "evenly_spaced",
"jobsPerPhase": 1
}
}
}
Use simultaneous to place all jobs in one phase or evenly_spaced to spread
equal phases across the paid window. jobsPerPhase must not exceed jobs and
must divide it exactly. Two jobs do not create a cluster: V5 has no membership,
topology, state replication, or public-service routing contract.
Sizing and processor selection
Omit placement for the open market, name an exact allowed pool, or state measured minimums:
{
"processorSelection": {
"mode": "exact",
"processorIds": ["processor-id"]
},
"minimums": {
"memory": "4GiB",
"storage": "16GiB",
"cpuSingleCoreScore": 1200
}
}
Minimums are filters, not reserved resources. Memory is total device RAM; storage is available space. Missing current evidence fails closed. Country, region, WAN-IP, allow/exclude, spread, distribution, and authored evidence profiles are not V5 fields.
The exact list is the allowed pool for each job. deployment.jobs does not
decide how many ids you may write, and the list does not have to be the same
length as the job count. One job can allow several processors:
{
"jobs": 1,
"processorSelection": {
"mode": "exact",
"processorIds": ["processor-a", "processor-b", "processor-c"]
}
}
Choosing an eligible processor from that whole pool is release-gated. The manifest accepts the list. Do not rely on a later id being selected when an earlier one is unavailable until that serving behavior is released. Capabilities and limits records the exact selection you can use today, including warmup.
When that selection is released, the order of processorIds is not placement
priority. In the example above, a stale heartbeat on processor-a and a hard
gate failure on processor-b can still admit processor-c when it is
eligible. A failed capacity minimum removes that processor and leaves the
other ids available. If none of the three is eligible, Liskov does not leave
the list for the open market. After the job is admitted, that processor stays
selected until a renewal or a permitted recovery admits a new occurrence. A
cheaper processor that shows up later does not move the admitted job.
If you recently registered a processor, put its Acurast address in
processorSelection.processorIds. Acurast lets a direct
Single.instantMatch assignment use that processor during its WarmupUntil
period; open-market selection waits until the processor is Active. The named
processor still needs current attestation, a compatible processor version and
runtime, a recent heartbeat, an available schedule, and enough funds. Publish
once, then check the deployment's processor assignment and runtime contact.
If Liskov reports a placement blocker, correct the named prerequisite before
trying again; repeating publication can reserve more Service Credits.
Renewal and updates
Lifecycle defaults to immediate update with the predecessor stopping when the
successor is ready. Use run_until_scheduled_end when overlap would be unsafe:
{
"renewal": {
"leadTime": {"mode": "fixed", "duration": "10m"}
},
"update": {
"timing": "next_renewal",
"existingJobs": {"mode": "run_until_scheduled_end"}
}
}
immediate, the default, starts the successor at the next executor pass,
including when only the artifact changed, and next_renewal starts it one
renewal lead before the running job's scheduled end. stop_when_successor_ready,
the default, stops the old job once the new runtime is ready, and
run_until_scheduled_end lets the old job run until its scheduled end. An
executor pass is the next time Liskov checks the Application and starts work
that is due. The renewal lead is how long before a job's scheduled end Liskov
starts the next job when you choose to wait. The scheduled end is the end
time registered for the current job's window.
Renewal is valid only for continuous execution. Lead time is automatic or
fixed; update timing is immediate or next_renewal.
Variables, secrets, logs, and Runtime SSH
Literal variables participate in the authored digest. Managed variables resolve at delivery time. Secrets name a server-side secret and an environment or file destination; secret material never appears in the manifest or effective policy.
{
"variables": [
{"source": "literal", "name": "LOG_LEVEL", "value": "info"},
{"source": "managed", "name": "API_ORIGIN", "required": true}
],
"secrets": [
{
"secretId": "api-token",
"required": true,
"destination": {"kind": "file", "path": "/run/secrets/api-token"}
}
]
}
Environment destinations cannot collide. Arrays contain at most 64 entries.
Managed logging has one authored switch: observability.logs.enabled.
Managed Runtime SSH is native-image-only and uses this policy arm:
{
"access": {
"ssh": {
"provider": {"kind": "liskov_managed"}
}
}
}
Keys are not embedded in V5 policy. Read the prepared V5 Managed Runtime SSH procedure before enabling the arm.
Exact reference and release identity
- Application Manifest V5 reference
- Capabilities and limits
- Contract RC:
sha256:549272988045e9357c4945850706569ed8dc7f0c6f419b7cf5c57d54b294bb10 - Manifest schema:
sha256:38ca88eefe599d9a13b0906fb7ae86be002fb7aa15767925a2fe11908fec95da