Skip to documentation
Documentation menu
On this page

AirPrompter documentation

Operate hosts with the CLI and the daemon

Pull, verify, apply, and inspect releases from a shell, run one sync loop per host with airprompterd under systemd, launchd, Docker, or Kubernetes, and carry releases and telemetry across an air gap.

Available

airprompter is one binary for the people who run hosts and pipelines rather than write agents. It shares the TypeScript SDK’s modules, so what verify refuses, a runtime refuses too. airprompterd is its daemon mode, airprompter daemon: one sync loop and one shared encrypted store per host, served to every SDK process on the host over a local socket, plus the host’s telemetry upload.

Before you install

  • The CLI ships as signed single-file executables for macOS, Linux, and Windows from the repository’s releases (airprompter-linux-x64, airprompter-darwin-arm64, and so on); each carries a provenance file naming the platform signature applied. If the release you need is not listed yet, cli/ builds the same binary from source with npm ci && npm run bundle.
  • The Agent key is read from AIRPROMPTER_AGENT_KEY (or the variable named by --api-key-env), never from arguments. Private keys come from files the CLI wrote with mode 0600.
  • Nothing the CLI prints is prompt text, at any verbosity. Releases are described as generations, digests, ids, models, variable names, and counts.

The commands

CommandWhat it does
pullFetch and verify the current release; write an encrypted .apbundle and a .meta.json sidecar; --check compares a vendored one and exits 3 when it is stale
verifyRun the verification chain on a bundle or a state directory and print the reasons
applyStage a bundle into the host’s store and activate it per the environment’s policy
statusActive and staged generation, lease, storage protection, spool depth, last upload; asks the daemon when one runs
doctorEvery reason this host is not serving the release it should, with the remedy
diffWhat a bundle would change against the active release, or against another bundle (--against)
unlockMake the staged release live on this host (--generation N to name it)
policy show / policy setShow or set the apply policy this host holds; a release may tighten it, only this loosens it
keygenGenerate a distribution or countersign key pair; refuses to write a private key inside a git checkout unless --allow-worktree
daemonairprompterd
export-telemetry / import-telemetryCarry the spool across an air gap as one file, idempotently
telemetry verify / telemetry validateProve the disk budget invariant on this machine; check a third-party writer’s segments line by line
devServe a directory of prompt files as a local registry while you edit; the conformance target
login / importSign in as a workspace member and turn a directory, export, or query result into reviewable prompt versions

Exit codes scripts can rely on: 0 ok, 1 refused (the reason on stderr), 2 usage, 3 stale (pull --check only), 4 partial (import only). --json prints one JSON document on stdout, always the last line; on a refusal it carries ok: false, the error, the exit code, and the step that refused.

Vendor a bundle in CI

# once, on a workstation outside any repository: the environment's distribution key pair
airprompter keygen --purpose distribution --out ~/.config/airprompter/prod
# register ~/.config/airprompter/prod.pub.json on the environment (Agent → Settings)

# in the build: the current release, sealed to that key
AIRPROMPTER_AGENT_KEY="$CI_SECRET" airprompter pull \
  --org org_… --agent agt_… --environment prod \
  --root ./airprompter-root.jwk.json --root-url https://<edge>/roots/prod/root.json \
  --distribution-key ./prod.pub.json \
  --out airprompter.bundle.apbundle

# in a scheduled job: fail when the vendored bundle falls behind
AIRPROMPTER_AGENT_KEY="$CI_SECRET" airprompter pull --check --max-behind 2 \
  --org org_… --agent agt_… --environment prod --out airprompter.bundle.apbundle

--root takes the environment’s pinned root public key or a full signed root document; a pinned key alone cannot name the signing keys, so pull also needs --root-url. airprompter diff new.apbundle --against old.apbundle compares two bundles with no store, slot by slot, and warns when a bundle goes backwards in generation, because every host refuses one. The repository also ships a GitHub Action, action/verify, that runs verify on every bundle a pull request commits.

Apply on a host

  1. Copy the bundle to the host; keep the distribution private key where the runtime reads it and nowhere else.
  2. airprompter verify <bundle> --org … --agent … --environment prod --root ./root.jwk.json --distribution-key ./prod.key.json and read the reasons.
  3. airprompter diff <bundle> --org … --agent … --environment prod --distribution-key ./prod.key.json to see what would change.
  4. airprompter apply <bundle> --org … --agent … --environment prod --root ./root.jwk.json --distribution-key ./prod.key.json.
  5. airprompter status --agent … --environment prod.

apply runs the identical chain a runtime runs (root, manifest signature, payload hashes, generation counter), stages into the inactive slot, and activates under auto; under unlock-required it stages and says so, and airprompter unlock makes it live. A generation below the stored one is refused unless --force, and then it is a forced downgrade stamped on evidence. Rolling a host back is ap.rollback() in the SDK or the rollback operation on the daemon’s socket; the CLI’s messages name an airprompter rollback command that is not in its command table in the current release. Pass --state-dir to match what the runtime was started with; the default is the OS state directory.

Expected result: status shows the applied generation active, and an SDK process on the host started against the same state directory serves it before touching the network.

Diagnose a host with doctor

doctor is the day-to-day first step when a host is not serving what the board says it should.

AIRPROMPTER_AGENT_KEY=… airprompter doctor --org org_… --agent agt_… --environment prod --root ./root.jwk.json --json

Nine read-only checks (source, edge_pointer, root, store, active_release, lease, key_protection, policy_pin, spool, daemon), each ok, warn, fail, or skip, printed with what to do next. It never creates a store, writes a file, or sends a heartbeat; with no key the source check is skipped, not failed. Exit 0 when no check fails.

Run the daemon

AIRPROMPTER_AGENT_KEY=… airprompter daemon \
  --org org_… --agent agt_… --environment prod \
  --root /etc/airprompter/root.jwk.json --root-url https://<edge>/roots/prod/root.json \
  --edge-pointer-url https://<edge>/g/<token>/generation.json --poll-seconds 30 --json

The daemon listens on <store>/daemon.sock (mode 0600; a named pipe on Windows). Attached SDK processes receive the verified release, generation events, and host-wide unlock and rollback; a process that finds no socket syncs in-process from its own store; a daemon that cannot obtain the store’s key does not listen. Logs are one JSON object per line on stderr and never carry prompt text. GET /healthz on the socket answers 200 with a verified release active and 503 without; airprompter status asks the daemon when it is there and doubles as a liveness probe.

With an Agent key the daemon also uploads the spool: every closed segment under the store’s telemetry directory, its own, the attached processes’, and any third-party writer’s, checked line by line first. Passes run every --upload-interval-seconds (300); --spool-budget-bytes sets the host budget (100 MiB); --no-upload leaves the spool on disk; --upload-sink otlp with --otlp-endpoint, --otlp-header 'name=$VAR', and --otlp-resource key=value sends the windows to your OpenTelemetry collector instead, with no Agent key needed on that path.

The daemon stops on SIGTERM within 20 seconds; it tells attached SDKs it is going, and they keep serving what they hold and reconnect.

Deploy manifests

The repository’s deploy/ directory holds a validated manifest per host type. Every one does the same things: the Agent key comes from AIRPROMPTER_AGENT_KEY in the service’s environment (a 0600 env file, a Secret, the service account’s environment), never from arguments; the state directory is per-service and 0700; the socket lives inside the store directory; application processes that should attach run as the same user with the same state directory.

HostManifestWhat to fill in
Linux, systemddeploy/systemd/airprompterd.service + airprompterd.env.exampleInstall the binary to /usr/local/bin, the env file 0600 to /etc/airprompter/airprompterd.env, the root key to /etc/airprompter/root.jwk.json, then systemctl enable --now airprompterd
macOS, launchddeploy/launchd/com.airprompter.airprompterd.plistlaunchctl bootstrap gui/$(id -u) …; on shared hosts wrap the command in a script that reads the key from the keychain rather than the plist
Windows servicedeploy/windows/airprompterd.xml (WinSW wrapper)The key in the service’s environment; state under C:\ProgramData\AirPrompter\state
Docker sidecardeploy/docker/Dockerfile, docker-compose.ymlThe daemon and the app share a state volume and a uid; the app’s SDK uses sync.mode: "daemon" and the same XDG_STATE_HOME
Kubernetesdeploy/kubernetes/daemonset.yamlOne daemon per node; the key from a Secret; the store on a hostPath the application pods mount read-write, running as the same runAsUser

The systemd unit is hardened (ProtectSystem=strict, ReadWritePaths=/var/lib/airprompter, NoNewPrivileges, address families limited to Unix and IP sockets). The Kubernetes DaemonSet runs non-root with a read-only root filesystem and all capabilities dropped, and probes with airprompter status --json.

A host that never calls home

An environment whose hosts have no route to AirPrompter is served by a file. On an environment whose sync mode is offline and whose distribution key is registered, the environment’s Settings offers Download update file: the promoted release, its payloads, and the environment’s root document, sealed to that key, good for 90 days by default. The console, verify, and apply all warn inside the last 30 days.

# on the host:
airprompter verify agt_…-prod-g12.apbundle --org … --agent … --environment prod \
  --root ./root.jwk.json --distribution-key ./prod.key.json
airprompter apply  agt_…-prod-g12.apbundle --org … --agent … --environment prod --root ./root.jwk.json --distribution-key ./prod.key.json

# telemetry back: pack what the runtimes on this host spooled
airprompter export-telemetry --org … --agent … --environment prod --out 2026-09-17.aptelemetry

# carry the file to a host that can reach AirPrompter, then:
AIRPROMPTER_AGENT_KEY=… airprompter import-telemetry --org … --agent … --environment prod --in 2026-09-17.aptelemetry

export-telemetry packs every closed segment with at least one valid row and moves the packed ones to exported/, so the next export packs only what is new (--keep leaves them). import-telemetry heartbeats once per instance the file names and posts each segment under its own name, so importing the same file twice counts nothing twice. A file for another agent or environment is a usage error before any network.

Re-key the distribution key at least every 90 days: keygen a new pair, register the new public key, download a fresh update file, and retire the old private key once the hosts have applied it.

Troubleshooting

verify refuses with unknown_signing_key

You passed a pinned key with no root document. Add --root-url, or pass a full root document to --root; for an offline host the update file carries the root document itself.

apply says the bundle is stale or backwards

A generation below what the host holds is refused; a rollback is ap.rollback() or the daemon’s rollback operation, never an older bundle. If you really mean to go back, --force stamps a forced downgrade.

status reports file_key under storage protection

The store key is a file beside the store. That is fine for a first run and is shown as a finding on the Fleet tab; production hosts use an OS keystore, KMS, or Vault through the SDK’s key provider.

The daemon is running but SDK processes sync in-process

They are not the same user, or not the same state directory. airprompter status shows attached clients; doctor names a stale socket.

Completion checklist

  • The binary you installed is the signed release for your platform, or a build you produced yourself.
  • AIRPROMPTER_AGENT_KEY is in a 0600 env file, a Secret, or the service environment, and never on a command line.
  • doctor exits 0 on every host.
  • Application processes attach to the daemon, or you have decided they should not.
  • Air-gapped hosts have a schedule for update files and telemetry files, and for re-keying.

Next steps

Pair the daemon with the resident TypeScript or Python integration, or replace hand-applied bundles with the fleet pattern when the estate grows.

Your Privacy Choices

We use advertising cookies and share limited information, such as a hashed email address when you sign up or subscribe, with advertising partners like Meta and Google to measure our ads. Some US state laws treat this as a "sale" or "sharing" of personal information.

Advertising cookies and sharing
On: ad partners can measure visits and sign-ups from their ads. Off: no ad tags load and we send nothing to ad partners.
Read our Privacy Policy