liskov-runtime-contact is the native bootstrap and process supervisor for
Liskov-managed workloads running in Acurast Cargo/PRoot images.
It discovers the active Acurast deployment and processor through the Cargo bridge, signs one Liskov runtime-bootstrap v2 request with the deployment's Ed25519 key and waits for bounded authenticated contact. When that bound response enables runtime environment retrieval, the helper signs a separate UID-bound v2 request, rejects endpoint or response substitution, validates every environment name, and captures those values before customer startup. It then becomes a child subreaper, starts the customer command in a dedicated process group, forwards supported signals, reaps descendants, and returns the customer's exact exit status or terminating signal after cleanup. If identity, signing, transport, or bootstrap validation fails, the customer command does not start.
When the signed bootstrap envelope contains the Liskov-owned x.pc extension,
the helper also emits bounded pre-contact evidence before bridge discovery and
once more on terminal contact failure. The envelope is read from
LISKOV_BOOTSTRAP only; the retired PROOF_SLIPWAY_BOOTSTRAP spelling is no
longer read. This bearer-authenticated evidence is
diagnostic only: it never authorizes command execution or replaces the signed
runtime-bootstrap gate.
- 64-bit AArch64 Cargo/PRoot workloads
- Acurast Android processor runtime 1.25.0 or newer
- an HTTPS Liskov core endpoint
The release binary is statically linked with Rustls and bundled Web PKI roots. It does not depend on OpenSSL or the image's CA store.
liskov-runtime-contact [--core-url URL] -- <command> [args...]
For example:
./liskov-runtime-contact -- /opt/my-app/bin/server --listen 0.0.0.0:8080The core URL is selected in this order:
--core-url URLLISKOV_CORE_URLhttps://runtime.liskov.proof.computer
runtime.liskov.proof.computer is the fleet's own control-plane name. It is
deliberately not the operator console hostname, so console protection rules,
maintenance, and edge changes cannot wedge running jobs.
Only HTTPS URLs without user information, a query, or a fragment are accepted.
BRIDGE_SOCKET is required and is supplied by the Acurast Cargo runtime.
The supervisor writes concise, non-secret diagnostics to stderr. It never logs bridge replies, signed request or response bodies, signatures, processor identity, pre-contact tokens, or the customer command.
After successful contact it also emits best-effort, deployment-signed Cargo diagnostic-v4 observations. These observations use one monotonic sequence for the runtime instance and include nested process-attempt counters. Delivery is bounded to five seconds per request with a bounded response and never changes workload execution.
Release 0.10.20 also ships a dormant, bounded cargo-baseline-v1 processor-
fact collector. It performs no reads, signing, DNS, or additional HTTP when the
authenticated bootstrap omits processorFacts, as current servers do. A valid
short-lived authorization is removed from the retained bootstrap before every
other consumer, must pin the running helper version and /proc/self/exe
digest, and can request only the due Android-corroboration, execution-surface,
or controlled-egress dimensions. Collection runs once in a silent detached
worker immediately before the first customer-process spawn and cannot affect
startup, supervision, signals, restarts, or exit status. The exact closed wire,
privacy, file, network, signature, and delivery contract is documented in
docs/processor-fact-contract.md.
Output forwarding starts only when the signed bootstrap response contains the
exact policy decision {"logging":{"enabled":true}} and
BLACKBOX_LOG_CONFIG validates against the same application, deployment, and
job. A signed runtime-environment value takes precedence over an inherited
process value. When neither contains the config, the supervisor uses the
signed /api/jobs/secret-bootstrap protocol to discover the exact job grant,
then requests only blackbox-log-config and decrypts its response through the
Acurast bridge. The transitional job-bound Lockbox bootstrap value is still
accepted when present, read only from LISKOV_LOCKBOX_BOOTSTRAP. Both paths
verify the response,
encrypted-payload, AAD, plaintext, and secret bindings before installing the
value in memory. This narrow lookup
lets the controller start before Runtime SSH without making the supervisor the
owner of customer secrets. Lookup failure is non-fatal to the workload and
access attachment.
The supervisor tees stdout and stderr to their normal local destinations, then forwards separately encrypted records through Liskov's canonical Blackbox sink protocol. UTF-8 chunks are labeled as text; binary chunks use base64url framing. Every encrypted record binds the stream, process attempt, monotonic output sequence, runtime instance, timestamp, byte length, and truncation state.
Capture uses 3-KiB chunks, a 256-KiB-per-second admission limit, a 128-item in-memory queue, at most 32 records and 256 KiB per request, five-second requests, 16-KiB responses, and at most ten network requests per second. It does not create a local durable spool. Queue or rate overflow produces only bounded encrypted dropped-byte and dropped-chunk evidence. Network failure, sink backpressure, invalid logging configuration, and the bounded 250-ms terminal flush never block, restart, terminate, or change the customer result. Customer output, the Blackbox DEK, factory token, request signatures, and environment values never enter Cargo diagnostics.
The server-owned loggingOutageCanary bootstrap flag is accepted only when
logging is already explicitly enabled and the bound Blackbox configuration is
valid. For an exact release canary it makes every log request fail locally,
without changing capture limits, local stdout/stderr, supervision, diagnostics,
or the customer result. It is not a customer-authored policy field and never
enables logging by itself.
When Runtime SSH is attached and logging is enabled, the server may additionally
authorize a closed diagnostics.providerLogs mode. sanitized emits typed
setup, ready, degraded, crash, and stopped lifecycle records. The temporary
raw_tailscaled_stderr mode adds only tailscaled stderr for an exact
server-selected canary until its signed expiry. Every raw line is converted to
valid UTF-8, capped at 8 KiB, and stripped of the in-memory auth key and
recognized credential forms before encryption. Runtime SSH has its own
64-KiB/s, 128-line, 1-MiB queue, but shares one ordered Blackbox writer with
customer output; customer records are always serviced first and origins never
share an encrypted batch. Provider logging failure cannot affect workload
startup, supervision, health, or the customer exit result.
| Status | Meaning |
|---|---|
2 |
CLI or configuration error |
70 |
Bridge, identity, signature, protocol, or permanent server rejection |
75 |
Transport failure or retry exhaustion |
126 |
The customer command could not be started |
On Unix, normal exits and terminating signals are propagated exactly after the customer process group and adopted descendants have been cleaned up.
The compatibility default is never: one customer attempt is run. A strict,
server-owned optional bootstrap block can select on_failure with either an
attempt limit from 0 through 10 restarts or the schedule end. Unknown or
malformed values fail closed to never. Restart delays use deterministic
equal jitter, a one-minute cap, a five-minute stability reset, and a five-second
schedule runway.
Non-default restart policy is currently limited to exact applications selected
by the Liskov control plane through LISKOV_CARGO_SUPERVISION_CANARY_JSON. The
variable is an internal fail-closed canary control, is removed before customer
startup, and is not a customer-authored policy surface. Managed Runtime SSH
receives its exact-job connector credential in the authenticated
runtime-bootstrap response and takes it out of that in-memory response before
customer startup. Both spellings of the bootstrap secret
(LISKOV_BOOTSTRAP and the no-longer-read PROOF_SLIPWAY_BOOTSTRAP), the
supervision canary control, and the reserved Runtime SSH environment credential
are removed from the captured customer environment. Runtime values cannot
reintroduce those protected names. The Lockbox bootstrap value is not one of them: it is
fetch metadata rather than a secret, so it stays deliverable and visible.
The customer command inherits the processor's Android process environment,
and four of those names are repaired before startup. They are repaired only
when the value would be invalid inside the Debian rootfs. TMPDIR is kept if
it names a directory the guest can write to, and otherwise becomes /tmp,
which is created 1777 if missing. LD_PRELOAD is kept only while every
object it names by path exists as a file in the guest. The loader fails the
whole list, so one missing object removes the variable. PATH defaults to
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin when it is unset
or empty, and HOME defaults to /root when unset. The repair runs on the
inherited values before the signed runtime environment is merged. A signed
value for any of these names therefore outranks both the inherited value and
the default, and is never itself repaired. No other inherited name is touched.
When anything was repaired, one runtime.environment.normalized supervisor
diagnostic names each changed variable and the reason, never a value.
For bounded release canaries whose Shell host does not retain stderr,
--diagnostic-exit-codes replaces status 70/75 with a non-secret stage
code. This flag is an internal canary interface, not a customer compatibility
surface:
| Status | Stage |
|---|---|
80–82 |
Bridge setup or deployment identity |
83–84 |
Deployment public key |
85–86 |
Assigned-processor binding |
87–88 |
Deployment signing |
89 |
Request construction |
90 |
Permanent server rejection |
91–92 |
Response validation or binding |
93–94 |
Runtime randomness or clock |
95 |
Retry exhaustion |
96 |
Runtime-environment endpoint construction |
97–98 |
Runtime-environment randomness or clock |
99 |
Runtime-environment signing |
100 |
Runtime-environment request serialization |
101 |
Runtime-environment transport |
102 |
Runtime-environment server rejection |
103–104 |
Runtime-environment response validation or binding |
The flag changes only failure reporting: contact remains fail closed and the customer command is never started after an error.
Liskov-controlled canaries may also use the hidden --bridge-probe flag. It
first reproduces Acurast's Cargo-native example call:
signer_publicKey with curve: "p256". That first connection receives the
remainder of the shared 15-second budget and is followed by the example's
deployment_assignedProcessors call. The probe then checks unique decimal
UInt request IDs and the helper's incompatible long request ID across
Liskov's stricter Cargo bridge surface, including a domain-separated harmless
Ed25519 signing call. It stops at the first decisive failure. The long-ID
comparison runs last and is evidence only. The probe emits only method,
ID-style, closed outcome, and optional numeric JSON-RPC code fields.
Retryable bridge failures and incomplete identity replies use a 250 ms first delay and then 2-second intervals before the helper generates its nonce and timestamp. The helper signs once per process and reuses the exact serialized request for all HTTP attempts. Identity discovery and HTTP contact each allow at most 30 attempts, share one 60-second elapsed-time ceiling, and each HTTP attempt has a 10-second timeout.
Pre-contact reporting is independent of this retry loop. The started and terminal reports each receive one HTTP attempt, a two-second timeout, an 8-KiB response limit, and strict response binding.
Each release publishes:
liskov-runtime-contact-v<VERSION>-aarch64-unknown-linux-musl
liskov-runtime-contact-v<VERSION>-aarch64-unknown-linux-musl.tar.gz
liskov-dropbear-2026.94-v<VERSION>-aarch64-unknown-linux-musl
liskov-dropbearkey-2026.94-v<VERSION>-aarch64-unknown-linux-musl
runtime-contact-release.json
SHA256SUMS
The raw helper, the two static Dropbear executables, and release-manifest v2
are attested separately. The manifest binds the immutable tag and source commit
to every exact digest and byte size. The archive contains the helper and its
fixed sibling liskov-dropbear and liskov-dropbearkey executables, plus this
README and the FSL-1.1-Apache-2.0 license. Runtime access verifies all three
executable digests before use and never installs packages inside the customer
image.
Verify the checksums before use:
sha256sum --check SHA256SUMSThe repository uses Rust 2024 with MSRV 1.85. Validation is offline:
cargo fmt --all --check
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo build --workspace --all-targets --locked
cargo test --workspace --all-features --lockedStable publication requires the repository's native AArch64 and emulated Cargo/PRoot release gates in addition to these host checks.
Developers with Docker can run an AArch64 candidate inside the exact maintained Liskov Debian rootfs without installing QEMU or PRoot on the workstation:
scripts/run-qemu-proot-runtime.sh --self-test
scripts/run-qemu-proot-runtime.sh -- /workspace/path/to/aarch64-candidate --version
scripts/run-qemu-proot-runtime.sh --profile netlink-denied --self-test
scripts/run-qemu-proot-runtime.sh --no-shim -- \
/workspace/tools/qemu-proot/managed-access-smoke.sh \
/workspace/candidate/liskov-dropbear \
/workspace/candidate/liskov-dropbearkey \
/workspace/candidate/test-ld-linux \
/workspace/candidate/test-libs \
/workspace/candidate/test-openssh \
/workspace/candidate/test-ssh-keygen \
/workspace/candidate/test-ncThe wrapper bind-mounts the invocation directory read-only at /workspace and
caches only the digest-verified rootfs under .cache/qemu-proot/. It runs the
container as the invoking non-root UID, keeps privileged-port binding disabled,
disables container SELinux relabeling so source files stay unchanged, and gives
the container only SYS_PTRACE, which PRoot needs. The inner command
builds the clean termux/proot v5.1.107.72 base identified by the Acurast
processor 1.27.0-rc1 APK and uses the APK's PRoot shape: fake root,
--kill-on-exit, --link2symlink, the /dev, /proc, /sys, and /dev/pts
binds, /root as its working directory, and the documented PATH and HOME.
The Liskov getifaddrs loopback shim is preloaded by default, as it is in the
generated Cargo launcher.
The managed-access smoke uses the release Dropbear binary and production
hardening flags. For that smoke only, it gives QEMU an unopenable argv[0] so
Dropbear takes its built-in straight-fork fallback: QEMU user mode cannot
redispatch Dropbear's per-connection /proc/self/fd AArch64 re-exec. Native
AArch64 execution retains the normal per-connection re-exec behavior.
processor-like leaves route netlink available. That matches Acurast's current
documentation, which says PRoot can expose Android's real interfaces through
netlink. netlink-denied is a deliberate seccomp fault profile which returns
EPERM only for socket(AF_NETLINK, ...) while leaving IPv4/TCP available. It
is useful for proving a constrained tailscaled patch or tailscale-rs mode,
but it is not a claim that every processor denies netlink.
This is a compatibility harness, not an Android security emulator. QEMU changes
CPU execution, upstream PRoot replaces Acurast's patched Android libproot, and
Docker cannot reproduce the processor app UID, SELinux policy, abstract bridge
services, Android interfaces, or Acurast's outbound connect interceptor. The
final processor canary remains required. Do not put auth keys in argv; provide
a mode-0600 file below the selected workspace and let the candidate consume the
file.
The managed-access smoke injects the same pinned static Dropbear and keygen companions as the release bundle plus the native ARM runner's test-only stock OpenSSH/netcat binaries, loader, and exact shared-library closure. It confirms port 22 is denied, starts the fixed loopback port-2222 Dropbear command, and uses stock OpenSSH through a local byte relay with a pinned host key. It performs no package-manager operation inside the maintained runtime image. The stock client bundle is not a release asset. The smoke also confirms killing the access sidecar does not change an independently running customer's exact exit.
After the offline and emulator profiles pass, operators with an approved customer-owned test tailnet can exercise official Tailscale control, DERP, and native SSH without paying for an Acurast job:
scripts/test-tailscale-saas-runtime-ssh.sh \
--yes-provider \
--credential-file /protected/path/tailscale-canary.jsonThe credential file parent must be mode 0700 and the file mode 0600. The
JSON contract is kind, tailnet, tag, oauthClientId, and
oauthClientSecret; the gate accepts only kind tailscale and tag
tag:liskov-runtime. It refuses a local Tailscale client which is offline or
logged into a different tailnet and never changes the local login.
The gate verifies the pinned official ARM64 archive, mints one tagged,
preauthorized, ephemeral, non-reusable 15-minute key, supplies it only through
a mode-0600 file, and removes that file immediately after authentication. It
then finds exactly one non-baseline tagged device, invokes tailscale ssh with
an argument array, verifies root and maintained-rootfs markers, stops the exact
named emulator container, and deletes only the exact persisted key and device.
Output is limited to booleans and counts; OAuth values, auth keys, tailnet
names, hostnames, device IDs, network maps, daemon logs, and SSH output remain
inside protected disposable state and are removed on exit.
This is a provider-real compatibility test, not the Android release gate. It does not reproduce the processor app UID, SELinux policy, Android interfaces, or Acurast's private outbound-connect interceptor, and it does not prove customer-workload continuity through a sidecar failure.
Runtime-contact 0.10.40 consumes the optional signed-bootstrap v2
secrets.customerRequired hint. It retrieves the exact job-bound customer
secret group before starting the customer command, independently of logging.
Required delivery or installation failure exits with code 70 and a redacted
runtime-secret diagnostic. Older bootstrap responses keep their existing behavior.
Environment values retain their exact strings. Absolute file destinations are installed as private 0600 files using directory descriptors, no-follow opens, staging and rollback. Environment changes follow successful file installation. Symlinks, traversal and reserved staging names are refused. Interrupted file replacements are recovered on the next start; release receipts alone do not prove process installation. Existing artifacts must be rebuilt to use this helper.
Coverage authorizations may opt the pinned native helper into networkSample
version 1 using networkProberUrl. Only the compiled HTTPS prober origin is
accepted. No authorization means no network sampling. The prober capability
is opaque; the helper never receives its HMAC key.
The complete sample has a 30-second cancellation deadline, decimal-MB transfer caps, no measurement retries and closed errors. UDP runs over IPv4 before bulk traffic; HTTPS tests both address families. Raw addresses are never serialized. The largest completed directional leg supplies throughput; sequence/timing records supply RTT, RFC3550 jitter and loss. A near-expiry authorization skips the sample to preserve the existing result-delivery budget. The coverage job window remains 120 seconds until live measurements support changing it.
The same authorization also enables inboundReachability version 1: the helper
binds one port in 20000-60000 across both families, asks the prober to dial it
once per family, and signs the 32-byte nonce that arrives over that inbound
connection. The signature is written back down the same socket, because a
signature that did not travel the measured path proves nothing about it. The
listener is closed when the check ends and there is no persistent port.
The check has its own 4-second budget, separate from the sample's 30 seconds, so a slow link is never the reason its own reachability went unmeasured. Every wait is two seconds and nothing is retried: a firewalled processor is the normal outcome and costs what a reachable one costs. The helper carries the prober's signed verdict verbatim and never writes one; it cannot, and it never learns which address the prober dialled.
The signed block, shared vector and catalog match the liskov-rs contract owner.
Existing envelopes omit the optional blocks and retain their canonical bytes.
Refs BKLG-20260914-fglr; ADR-0139.
From v0.11.0 this repository is licensed under the Functional Source License,
Version 1.1, with Apache-2.0 as the future licence (FSL-1.1-Apache-2.0): each
version becomes available under Apache-2.0 two years after its release. Every
release tag before v0.11.0 remains under Apache-2.0. See LICENSE.
The job-facing liskov.discovery.v1 contract remains part of the unreleased
V6 private endpoint path. It does not activate V6 or create a tailnet.
The application declares its named HTTP service and local port; Liskov returns
observed publication URLs. A hostname is not proof of peer reachability.
For a Tailscale workload with declared endpoints the helper exports
LISKOV_DISCOVERY_FILE, LISKOV_PEER_PROXY (a loopback socks5h:// proxy), and
LISKOV_RUNTIME_INSTANCE_ID. The file appears after the first successful
signed health refresh. It is atomically replaced; unavailable or invalid
responses preserve the previous snapshot and its original timestamp.
The snapshot contains schema, applicationUid, self, opaque revision,
observedAtMs, and peers[].{jobId,instanceId,services}. Named services contain
protocol and named endpoints with provider, URL (nullable), observed state and
observation timestamp. Limits are 256 peers, 32 endpoints per peer, 512 KiB.
Health diagnostics include discovery version, last-success time and refresh
failure, allowing operators to distinguish an empty directory from a stale one.
The proxy is not enabled for SSH-only attachments. HTTP/model/global proxy
variables are not modified. Applications retain their own authentication.
For jobs without the v1 Tailscale publication path, the helper supplies
LISKOV_RUNTIME_INSTANCE_ID, LISKOV_PEER_ADVERTISEMENT_FILE, and
LISKOV_PEER_DIRECTORY_FILE before starting the native workload. It does
not supply LISKOV_DISCOVERY_FILE in this mode. This is helper-owned runtime
metadata and does not change V5 policy or its authored variables. A missing
advertisement file means the workload has not opted in.
To opt in, atomically replace the advertisement file with one JSON object no larger than 4 KiB:
{
"schema": "liskov.peer-advertisement.v2",
"service": "sync",
"port": 8841,
"ready": true,
"tlsFingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}The service uses HTTPS in this version. The fingerprint is SHA-256 of the
listener's leaf certificate DER. The helper also accepts optional
peerSyncLastSuccessAtMs and peerSyncFailureCode
(none, connect, tls, auth, identity, exchange, or
discovery). These sync summaries are application-reported, not proof of
platform delivery or peer reachability. It refuses extra fields, symlinks,
oversized files, invalid ports, and invalid fingerprints. The helper supplies
verified app/job/runtime identity itself; the workload cannot advertise an IP,
URL, credential, or identity. An attempted direct advertisement on a v1
Tailscale job reports a conflict and retains v1 discovery.
A malformed, oversized, or symlinked local file still produces a signed v2
health beat with discoveryVersion: 2 and only a bounded
peerAdvertisementError in place of endpoint attributes. Liskov can retain
that refusal and return typed unavailable; the helper cannot write a
complete directory from this beat or extend the last good lease. A missing
file remains no opt-in, while a v1 Tailscale conflict keeps its v1 request.
Each health beat reads the current file and signs the v2 request attributes.
A complete, current liskov.discovery.v2 response replaces the private
directory file atomically. The snapshot includes a lease and each peer's
expiry; consumers must stop using expired endpoints. A failed, stale,
unsupported, or oversized response leaves the last good bytes and original
expiry intact and reports a bounded failure code. The helper removes the
advertisement before each supervised child start, so a restarted child must
opt in again. Direct discovery is advisory; applications still authenticate
peers and pin each advertised TLS certificate.
On child exit the helper stops reporting readiness and removes the input. An advertisement already accepted by the server retains only its original lease, at most 90 seconds from that health beat; the exited child's file cannot renew it.
discoveryLastSuccessAtMs retains the v1 meaning: it is the server's
observedAtMs on the last accepted snapshot, rather than a local receive
timestamp. The helper uses that value to reject delayed snapshots.
A manifest opts in with the signed runtime-env literal
LISKOV_APPLICATION_READINESS=v1. The helper reads that name only from the
signed map. Any other value, including the same name in the inherited
processor environment, leaves health beats and the child environment unchanged.
When the literal matches, the helper gives the child
LISKOV_APPLICATION_READY_FILE: an absolute path in the same private 0700
directory as LISKOV_PEER_ADVERTISEMENT_FILE. A forged ready-file path in the
signed map is replaced by that path. The child creates the file when it can
serve. The helper removes it before each supervised child start, so a
restarted child is pending until it writes the file again.
Every runtime.health then carries one scalar, applicationReadiness. The
value is pending until a regular file of at most 4 KiB is at that path, and
ready after. The helper ignores the bytes. A missing file, a symlink, a
FIFO, or an oversized file stays pending, and a symlink is not followed.