CLI Air-Gap Guide

Audience: operators running the Stella Ops CLI in offline or air-gapped (sealed-mode) environments.

This guide covers the offline usage patterns for the Stella Ops CLI: importing and verifying offline kits and mirror bundles, checking sealed-mode status, and the determinism rules every command must obey when no network egress is permitted. For the platform-level policy behind these commands, see the air-gap overview and sealing and egress.

Offline kit commands

Prerequisites

Common commands

The stella airgap group exposes import, seal, unseal, status, diff, time-anchor and export-evidence. Two verbs this guide previously listed — verify-bundle and list — were never registered on the command group and are removed here rather than left as commands that cannot resolve.

Air-gap posture

stella airgap status reads Platform’s environment-state custodian (GET /api/v1/platform/environment-state/airgap-seal/{scope}) and interprets the airgap-seal document with the family’s standard interpreter. It reports one of three answers, and they are deliberately distinguishable:

AnswerExitMeaning
declared0A document exists; isSealed is a measured value.
not-declared0Nobody has declared posture for this scope. This is not “unsealed”.
unreadablenon-zeroThe posture could not be established. An error, never a value.

Do not treat an environment as sealed or unsealed on the strength of a not-declared or unreadable answer. stella airgap seal is the audited writer of that document, and the Console’s air-gap indicator reads the same one.

Time-anchor status

stella airgap time-anchor status --scope installation --trust-roots ./tsa-roots.pem --output json

With --trust-roots, status binds that public certificate bundle to the central timestamp-authority trust purpose and uses the same stored-proof verifier as the enforcement consumers. The RFC3161 signature and custom-root chain must verify, and the recorded anchor time, token digest and signer fingerprint must agree with the proof. A wrong explicit format is refused; omitted format keeps the existing schema default of RFC3161, which strict token decoding independently checks. Certificate revocation is not established by this verification.

A failed requested verification returns exit 1, availability: "Unreadable", staleness: "Unknown", anchorTime: null, verified: false and a failure reason. It does not retain the raw document’s freshness result. Successful verification reports verificationRequested: true and verified: true; the staleness grade then describes the verified anchor against its configured budget.

Without --trust-roots, status preserves its read-only reporting mode: it shows the recorded document and its calculated age without cryptographic verification. JSON reports verificationRequested: false, verified: false and reverification: null; human output says it was not re-verified. These recorded values are not verification evidence. An absent scope remains NotDeclared with unknown staleness and exit 0; an unreadable custodian result exits 1. An absent scope also remains a measured absence when trust roots were supplied, with verified: false because there was no proof to check.

See Timestamp public trust for the central purpose boundary and offline verification limits.

Comparing manifests (stella airgap diff)

stella airgap diff <base> <target> compares two OfflineKit envelope manifests — the JSON that GET /api/offlinekit/v1/manifest returns and that POST /api/offlinekit/v1/validate accepts. Both sides are read from local files, so the command needs no network.

# Human-readable inventory delta
stella airgap diff ./manifest-2026-02.json ./manifest-2026-03.json

# Machine-readable, with an explicit `identical` flag
stella airgap diff ./manifest-2026-02.json ./manifest-2026-03.json --output json

# Restrict the artifact comparison to one declared domain
stella airgap diff ./base.json ./target.json --component scanner

What it reports: changed bundle header fields (version, channel, kind, bundleId, baseBundleId, isDelta, capturedAt, releaseManifestSha256), and per artifact whether it was added, removed or changed (sha256, size, domain, mediaType, generation), plus an unchanged count. Ordering is ordinal by artifact name, so two runs over the same inputs produce identical output.

--component filters artifacts by their declared domain (for example scanner); all compares every domain. Bundle-header changes are always reported, because a filtered view that hid a channel or delta-base change would hide the reason the artifact set moved.

What it deliberately does not do. diff does not verify signatures, does not check DSSE custody and does not hash any asset. Validation belongs to the owner (POST /api/offlinekit/v1/validate); a clean diff says two inventories match, not that either bundle is trustworthy.

Exit codes for this verb:

ExitMeaning
0Both manifests were read and compared, whether or not they differ.
non-zeroThe comparison could not be performed. Nothing was compared.

A difference is a result, not a failure. A comparison that never happened is a failure, and it must never be readable as “no differences” — which is why a missing file, malformed JSON, a duplicate artifact name or a foreign manifest lineage all exit non-zero and print no diff. Use the identical field in --output json to gate a script on sameness.

diff refuses the other manifest lineages in StellaOps.AirGap.Bundle by name. That refusal is not pedantry: BundleManifest and BundleManifestV2 both declare schemaVersion "2.0.0" with disjoint shapes, so the field an implementer would dispatch on first cannot tell them apart.

Determinism & offline rules

Exit codes

Exit codes are per verb, not global. The commands documented above follow their handlers:

For any other verb, read the code the command itself returns rather than assuming a shared scale; this guide does not assert one it has not verified.

Logs

Tips

See also