Build once: portable runtime bundles¶
vq runtime-bundle provides local-file, HTTPS and SSH artifact transport for #161.
A product-owned builder compiles and qualifies a relocatable runtime once;
Queue packages, authenticates, publishes and deploys those files. Deployment
has no installer/compiler fallback. It requires an explicit target profile
qualified on the intended compute-node class.
This is a separate store format. It does not change scheduler configuration, legacy source-SHA venv slots or automatic runtime selection. A job binds to a deployed generation only when it asks to; see Running jobs on a deployed generation. Passing an existing legacy runtime root fails before changing it. Existing site providers must adopt this identity contract before they can support same-source rebuilds; this feature alone does not repair their artifacts.
Identities and provenance¶
Identity |
Meaning |
|---|---|
|
Full actual source revision, attested by the trusted publisher |
|
SHA256 of the complete canonical build description, including recipe, dependency profile and target requirements |
|
SHA256 of the bundle bytes |
|
SHA256 of the canonical manifest, binding build provenance and the complete file inventory to the archive |
Changing dependencies or configuration at the same source revision produces a different build ID and artifact ID. Both generations can coexist; no fake source SHA or overwrite is needed. The queue does not infer provenance from a source directory or independently authenticate Git history. Product qualification and the operator’s authenticated publisher attestation supply that evidence.
An example build description, stored outside the product checkout:
{
"schema_name": "vq.runtime-build/1",
"program": "example-runtime",
"source_revision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"recipe_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"dependency_sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"target": {
"os": "Linux",
"arch": "x86_64",
"isa": ["sse2"],
"abi": {
"libc": "glibc-2.31",
"python": "none",
"blas": "none",
"mpi": "none",
"launcher": "none"
},
"modules": []
},
"variant": "generic",
"toolchain": "product-builder/toolchain-1",
"compiler_flags": ["-O2"],
"publisher": "qualified-builder",
"build_receipt": "build-record-1",
"qualification_receipt": "qualification-record-1",
"smoke_argv": ["bin/runtime", "--self-test"]
}
Replace every example digest with the actual authenticated identity. The recipe
and dependency digests should cover the complete product-owned inputs, including
dependency revisions and configuration. Their associated receipts remain in
operator-controlled storage. ABI values use exact equality, conservatively;
none explicitly means the artifact has no such external requirement. ISA
and module requirements must be subsets of the qualified target’s capabilities.
The target profile has the same shape as target above. The mandatory ABI
fields cannot be omitted. Generic descriptions reject -march=native/-mcpu=native.
Queue cannot infer minimum ISA or ABI safety by inspecting arbitrary binaries.
Package, trust, publish and deploy¶
Use absolute output/store/target paths. The runtime directory must contain only
distributable files: exclude credentials, signing keys, build workspaces and
operator configuration. Packaging creates a new directory containing
bundle.tar and manifest.json. No compilation or smoke execution occurs yet.
vq runtime-bundle pack /opt/build-output/example-runtime \
--description build.json --output /opt/artifact-staging/example-runtime
The printed artifact ID is a canonical manifest digest, not the hash of its
pretty-printed JSON file. It is not automatically trusted. After authenticating
that exact manifest with the publisher through an independent trusted channel,
the operator adds its digest to an external trust.json:
{
"schema_name": "vq.runtime-trust/1",
"publishers": {
"qualified-builder": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
}
}
Replace the example pin with the reviewed artifact ID. A checksum downloaded alongside a bundle does not authenticate it. Instead of pinning every digest, a policy can trust a publisher’s signing key; see Signed bundles. Trust is rechecked during publication and each deployment, so removing a pin or a signer key prevents a new apply. Existing running programs are not revoked or stopped.
vq runtime-bundle publish /opt/artifact-staging/example-runtime \
--store /opt/runtime-artifacts --trust trust.json
vq runtime-bundle deploy ARTIFACT_ID --store /opt/runtime-artifacts \
--root /opt/portable-runtimes/example-runtime --trust trust.json \
--target-profile compute-class.json --plan
vq runtime-bundle deploy ARTIFACT_ID --store /opt/runtime-artifacts \
--root /opt/portable-runtimes/example-runtime --trust trust.json \
--target-profile compute-class.json --apply
Plan is the default, reads and verifies evidence, and makes no target changes.
Its JSON lists compatibility blockers. Apply rechecks those gates under the
existing physical lifecycle locks for both the store and target. It extracts
into staging, checks every declared file, publishes a generation, runs the
trusted product’s smoke executable at its final prefix, and checks that the
smoke did not alter the payload. Only then does an atomic selection receipt
change current and previous together.
The smoke command runs as the invoking operator; this is not a sandbox. Its timeout defaults to 30 seconds (maximum 120); a timeout kills its process group. Product probes should check native libraries, data lookup and relocation, avoid background processes, and make no changes to the runtime. Standard Python and dynamic-library path overrides are removed from the smoke environment.
artifact-store/
format.json
artifacts/<artifact_id>/bundle.tar
artifacts/<artifact_id>/manifest.json
artifacts/<artifact_id>/manifest.json.sig # optional publisher signature
target-root/
format.json
selection.json # current and previous artifact IDs
generations/<artifact_id>/manifest.json
generations/<artifact_id>/verification.json
generations/<artifact_id>/payload/
The same artifact can be applied to multiple independent compatible prefixes.
To roll back, deploy a retained artifact ID with --apply. Consumers must pin
the returned absolute generation payload path; resolving the mutable selection
again during a running job would not preserve that pin. This increment retains
all generations and does not integrate with automatic queue pinning or cleanup.
Interrupted publication never installs partial artifact bytes. Repeating the same operation verifies the existing identity; it never overwrites a damaged artifact or payload. A lost acknowledgement or missing verification receipt is reconciled by checking the retained contents and rerunning the smoke. A failed smoke leaves the current selection intact and may leave an unselected generation for diagnosis. A killed process can leave unpublished staging directories; no automatic deletion of unknown directories is performed.
Signed bundles¶
Pinning each artifact ID means editing the trust policy for every new build.
A vq.runtime-trust/2 policy can instead name each publisher’s OpenSSH
public keys:
{
"schema_name": "vq.runtime-trust/2",
"publishers": {},
"signers": {
"qualified-builder": ["ssh-ed25519 AAAAC3...EXAMPLE qualified-builder 2026"]
}
}
Replace the example with the publisher’s real public key line. Get it
through an independent trusted channel, as you would a pin. Each entry is one
line as in a .pub file: the key type, the base64 key and an optional
comment.
Accepted types are
ssh-ed25519,ssh-rsa, theecdsa-sha2-nistp*types, and the security-key (sk-) forms.Options such as
cert-authority, additional lines and control characters are refused.
What is trusted. Pins and signers can be combined. A manifest is trusted
when it is pinned for its publisher (build.publisher), or when its signature
verifies with one of the keys listed for that publisher. A key listed for
another publisher does not count.
Compatibility. Schema 1 policies keep their meaning. A schema 1 policy
with signers is refused, and earlier vq versions refuse schema 2, so a
mixed fleet fails closed.
Signing a bundle¶
The publisher signs after packing:
vq runtime-bundle sign /opt/artifact-staging/example-runtime \
--key /opt/builder-keys/runtime-signing
signchecks the archive against the manifest.It runs
ssh-keygen -Y signover the canonical manifest: the bytes whose SHA256 is the artifact ID. The signature namespace isruntime-bundle@vibe-qc.com, so a signature made for any other purpose does not verify here.It writes
manifest.json.sigbesidemanifest.json, replacing any earlier signature.
--key is a private key file, or a public key whose private half ssh-agent
holds. ssh-keygen may ask for the passphrase. The key never enters the bundle
or the store.
Checking a signature by hand¶
vq writes manifest.json canonically, so you can check a signature without
vq. Put the line qualified-builder ssh-ed25519 AAAAC3... in an
allowed_signers file and run:
ssh-keygen -Y verify -f allowed_signers -I qualified-builder \
-n runtime-bundle@vibe-qc.com -s manifest.json.sig < manifest.json
How vq verifies¶
The host that runs publish, fetch or deploy needs OpenSSH 8.1 or later, the
first release with ssh-keygen -Y. Compute nodes do not verify signatures,
and pins work everywhere. vq tries each listed key on its own and does not
echo ssh-keygen’s output.
Each of these refuses, just as an unpinned manifest does:
a missing, malformed or wrong signature;
an unlisted key;
a signature made in another namespace;
no
ssh-keygen, or one without-Y.
A deploy that a signature trusted records signer in its plan and in
verification.json. The value is the key’s SHA256 fingerprint, as
ssh-keygen -l prints it. A pinned deploy records no signer.
Signatures in the store¶
First publication copies the signature into the store as
artifacts/<artifact_id>/manifest.json.sig.Republishing on a signature. When the bundle’s signature is what trusted it on this host, the stored signature is replaced. A store first filled on a pin can get a signature this way.
Republishing on a pin leaves the stored signature as it is.
Signing in place. You can also sign a published artifact directory directly.
Rotating a key¶
Add the new key beside the old one.
Sign new bundles with the new key.
Remove the old key.
Once the old key is removed, new publish, fetch and deploy operations refuse artifacts that only it signed; re-sign or pin those. Deployed generations, the selection and running jobs are unaffected. There is no revocation list or expiry: editing the policy is the only control.
Fetch from an HTTPS store¶
An HTTPS server can expose the published artifact store layout above. Fetch an independently authenticated artifact ID into a local store, then use the same deploy plan/apply commands:
vq runtime-bundle fetch ARTIFACT_ID \
--url https://artifacts.example.org/runtime-store \
--store /opt/runtime-artifacts --trust trust.json
The server must serve artifacts/ARTIFACT_ID/manifest.json and
artifacts/ARTIFACT_ID/bundle.tar beneath --url. The requested ID must already
be pinned in the external trust policy, or the policy must name signers.
Without a pin, fetch also downloads artifacts/ARTIFACT_ID/manifest.json.sig
(at most 64 KiB) and verifies it; a pinned fetch requests no signature.
Fetch authenticates the manifest and
checks its exact identity before requesting the archive, then verifies the
archive digest and complete file inventory before normal atomic publication.
Downloaded checksums cannot add trust. Fetch performs no compilation, smoke
execution, deployment or runtime selection.
TLS certificate and hostname verification are mandatory. The default system
CA roots are used; --ca-file /opt/operator-config/artifact-ca.pem selects an
operator-provided CA PEM for a private store. Redirects and HTTP downgrades are
refused. URLs containing credentials, queries or fragments are refused, and
transfer errors do not echo the URL or server response.
For a store requiring mutual TLS, supply an operator-managed client certificate chain and private key from external files:
vq runtime-bundle fetch ARTIFACT_ID \
--url https://artifacts.example.org/runtime-store \
--store /opt/runtime-artifacts --trust trust.json \
--ca-file /opt/operator-config/artifact-ca.pem \
--client-cert /opt/operator-config/artifact-client.pem \
--client-key /opt/operator-config/artifact-client-key.pem
The certificate chain must be PEM. The private key must be unencrypted; keep
it in an operator-protected file. Omit --client-key when the certificate PEM
also contains the key. A key without --client-cert is refused. Encrypted or
invalid keys fail without prompting, and credential-loading errors do not echo
file paths or contents. Credentials are read only when a download is needed,
so verified offline reuse does not require the original client files. They
are never copied into the artifact store or deployment. Client authentication
does not replace the artifact pin, and server certificate and hostname
verification remain mandatory. These TLS options apply to --url only.
For a store using bearer authentication, supply --bearer-token-file with an
external, owner-only regular file (for example mode 0600). The file contains
one ASCII token, at most 16 KiB, with an optional final LF or CRLF. Whitespace,
control characters, empty tokens and symlinks are refused. Tokens use letters,
digits, -._~+/ and optional trailing = padding. Use the token issued by the
artifact service, with read access limited to the required store:
vq runtime-bundle fetch ARTIFACT_ID \
--url https://artifacts.example.org/runtime-store \
--store /opt/runtime-artifacts --trust trust.json \
--bearer-token-file /opt/operator-config/artifact-store-token
The token is loaded only for an actual download and sent as an Authorization: Bearer header on the manifest and archive requests. It can be combined with
the CA and mutual TLS options above. Redirects remain refused; authentication
failures do not retry anonymously or echo response bodies, token contents or
credential paths. Verified offline reuse needs no token file. The token is
never saved in the artifact store or deployment, and vq does not automatically
reuse its administrative token. This option applies to --url only.
Downloads stream into private temporary staging outside the store and its
lifecycle lock. The manifest limit is 2 MiB; the default archive download limit
is 4 GiB. Set --max-download-mib to change the archive cap, up to 65,568 MiB
(64 GiB plus 32 MiB of archive overhead). The cap applies even if the server
omits Content-Length. --timeout sets the connection/read socket timeout
(default 30 seconds, maximum 120), rather than a total transfer deadline.
Interrupted or refused downloads are discarded without publishing partial bytes. Repeat the same command to retry the full transfer under the same immutable identity. If the local artifact already exists, fetch rechecks its current trust and complete contents and reuses it without contacting the server. A damaged local artifact is refused rather than overwritten. Fetch rechecks trust before publication, so revoking a pin during download prevents publication. This command is a client; it does not start a server or change network exposure.
Fetch from a store on another host over SSH¶
A store that vq runtime-bundle publish filled on a builder host can be read
as it is from a configured host, over the same SSH connection vq already uses
for that host. No server is needed:
vq runtime-bundle fetch ARTIFACT_ID \
--from-host builder --remote-store /srv/runtime-store \
--store /opt/runtime-artifacts --trust trust.json
--from-host names a [hosts.*] entry; its ssh destination and vq’s
connection, keep-alive and batch-mode options are used. --remote-store is
the absolute store root on that host: plain ASCII, without control characters
or . and .. components. --url and --from-host are mutually exclusive;
--ca-file, --client-cert and --client-key apply to HTTPS only.
The gates are the HTTPS fetch’s, in the same order:
Before any connection is made, the requested ID must be pinned in the local trust policy, or the policy must name signers.
An artifact already published locally is rechecked and reused without connecting.
The manifest is read and authenticated, by its pin or by the signature read beside it, before the archive is requested.
Both files go to private staging, and the normal publication checks the archive digest and the complete inventory.
SSH adds no trust: the bytes are exactly as untrusted as a download, and only the pin or signature and the digests decide.
Each file is read with head -c LIMIT+1 on the remote side, so an oversized
file is stopped at the source and the extra byte is caught locally. The
manifest limit is 2 MiB, and the archive limit is --max-download-mib. Over
SSH, --timeout is the longest a transfer may go without data before it is
stopped. A stalled or failed transfer kills the whole local ssh process group
and publishes nothing. The remote’s own error output is not echoed, since it
can name hosts and paths. Rerun the same command to retry.
Running jobs on a deployed generation¶
A daemon-host job can be bound to a generation of a deployment target:
vq submit HOST --runtime-root /abs/runtime -d inputs -- \
sh -c '"$VQ_RUNTIME_PAYLOAD"/bin/program input.inp'
vq submit HOST --runtime-root /abs/runtime --runtime-artifact SHA256 -d inputs -- ...
At submit, the execution host resolves the deployment’s current selection,
or the generation --runtime-artifact names. It accepts it only as
deploy left it: the generation’s manifest has that identity, its
verification receipt says it verified, and its payload is an operator-owned
directory nobody else can write. The job records it as runtime_bundle
(root and artifact_id). A refused root submits nothing.
Before the job starts, the daemon checks that same generation again and runs
the job with VQ_RUNTIME_PAYLOAD (its payload directory) and
VQ_RUNTIME_ARTIFACT (its identity). It never substitutes another
generation. A later deploy that moves the selection, or a rollback, changes
only what later submissions bind. If the bound generation is gone or no
longer verifies, the job fails with “bound runtime generation unavailable
before dispatch” instead of running on something else. A generation is
removed only by prune (below), which keeps every generation a queued or
running job in this host’s queue is bound to.
vq status shows a runtime: line, the fleet console names the generation in
the software column, and vq resubmit keeps the same generation. The
binding is part of the submission’s identity for --idempotency-key.
Without --runtime-root nothing changes.
Rewriting {runtime} in the command is left to wrappers such as
vq benchmark measure; the queue only exports the two variables.
On a scheduler target¶
A scheduler job can be bound too, but its driver cannot see the cluster’s filesystem, so the generation is checked on the compute node instead:
vq submit TARGET --runtime-root /cluster/runtime --runtime-artifact SHA256 -d inputs -- \
sh -c '"$VQ_RUNTIME_PAYLOAD"/bin/program input.inp'
--runtime-artifact is required, because the driver does not read the
cluster’s selection. The root is a plain absolute path on the cluster
(no ..), recorded as given, and nothing is checked at submit.
Before the command runs, the job script checks the generation as deploy
left it:
the generation directory,
manifest.json,verification.jsonandpayload/are not links;the SHA256 of
manifest.jsonis the identity:deploywrites the manifest as canonical JSON, so its digest is the generation’s identity;the verification receipt records
"verified":truefor thatartifact_id;payload/is not group or world writable.
The node needs sha256sum or shasum; with neither, the check fails. The
payload’s owner is not checked, because the account that deployed it need
not be the job’s.
If any check fails, the command never starts. The job fails with exit code
126, and its stderr.log reads “vq: bound runtime generation ID unavailable
on this node: REASON; command not started”. Its resource receipt records
runtime_generation_unavailable. The job never falls back to another
generation. Each element of a scheduler array checks before it runs. The
job’s environment and the runtime: status line are the same as on a
daemon host; a broker still refuses --runtime-root.
Listing deployed generations¶
vq runtime-bundle generations --root ROOT [--json] lists a deployment root’s
generations, read-only. Each entry gives the artifact ID, the program from its
manifest’s build.program, and whether it is verified. Verified is the
proof that verified_generation gives and a bound job relies on: the manifest
has that identity, and the deployment receipt says it verified. The current
and previous selections are marked. The JSON form (vq.runtime-generations/1)
is what vq placement fleet --runtime-root reads from a daemon host. See
placement plans.
Pruning old generations¶
Every deploy leaves its generation in place, so a target root grows with
each release. prune removes the generations nothing keeps:
vq runtime-bundle prune --root /abs/runtime # plan only
vq runtime-bundle prune --root /abs/runtime --keep SHA256 # also keep one
vq runtime-bundle prune --root /abs/runtime --apply
A generation is kept when any of these holds:
it is the current or previous selection (the rollback target);
a queued or running job in this host’s queue is bound to it;
it is named with
--keep, as its full SHA256 identity.
A job’s binding counts whether its recorded root is this path or resolves to
it, and multi-user queues are included. A finished job does not keep its
generation. Every other generation is listed as prune, with its size and
the total reclaimable bytes. The plan changes nothing.
--apply takes the same deployment lock as deploy and reads the selection
and the queue’s bindings again before removing anything. Each prunable
generation is first renamed to generations/.pruning-<id>-<pid>, so a job
that resolves it afterwards fails closed rather than reading a half-removed
tree. It is then removed without following links. An interrupted prune’s
leftover is removed by the next --apply. Entries that are not generations,
such as deploy staging, are left alone. The artifact store is untouched, so
deploy brings a pruned generation back (and selects it).
Limits:
A scheduler job binds a generation on the cluster from its driver’s queue, which a prune run on the cluster cannot see. Name those generations with
--keep.A submission that names an old generation with
--runtime-artifactwhile a prune removes it fails before dispatch; it never runs on something else.A finished job resubmitted later needs its generation deployed again.
Limits and remaining qualification¶
The format accepts regular files and optional directories, at most 10,000 files and 64 GiB unpacked. Symlinks, hard links, sparse/special files, traversal, duplicate members, privileged modes and missing/unlisted files are refused. Builders must dereference relocatable links safely before packaging; arbitrary venvs and vendor archives are not automatically portable. The local fixture builds a small native executable plus shared library and data once, removes the build prefix, and deploys the identical artifact to two new prefixes. That verifies the mechanism, not vibe-qc, ORCA or CRYSTAL qualification.
Follow-ups include other client credential adapters, build-host scheduling, product/vendor import adapters, and scientific and MPI smoke receipts on real compute-node classes. No production host has been changed by this source implementation. See scheduler runtime deployment for the existing site-provider workflow, and resource estimation for the companion preview foundation.