apiVersion: node-provider.peaq.network/v1alpha1) into an ordered, auditable install that provisions a machine as a node. All provider-specific complexity lives in the manifest; the runner only understands the schema’s generic vocabulary: phases, steps, prompts, captures, secrets, and verification probes. It never hardcodes any provider’s setup.
Provisioning ships in @peaqos/peaq-os-sdk 0.5.0+ (JS/TS) and peaq-os-sdk 0.5.0+ (Python), and runs on the machine itself.
Design invariants
- Verification, not exit codes, defines success. A machine is a live provider only when the manifest’s success probes pass.
- Secrets never leak. Secret inputs, secret captures, and per-step redaction are applied before anything is streamed, logged, or persisted.
- Sudo is explicit and scoped. Auto mode elevates only the commands the manifest marks, and only within the operator’s granted allowlist.
- Handoffs are real stops. Owner-action steps block until the owner confirms; a confirmation is never synthesised.
- Payout context defaults to the machine wallet, never the operator’s. The runner exposes the address as generic context; the manifest decides how it maps into commission fields.
The lifecycle
The namespace isclient.provisioning in JS/TS and the peaq_os_sdk.provisioning module in Python; every method is also importable standalone.
Fetch a manifest
Manifests are hosted on a public repo and pulled on demand. The repo base URL is configuration, never hardcoded. The JS SDK is YAML-agnostic (pass a parse function from a YAML library you already depend on); the Python SDK parses YAML itself.- Version: pass an explicit
versionor resolve"latest"; the concrete version is read back from the document and recorded for reproducibility. - Integrity: pass an expected digest (
expectedDigestin JS,checksumin Python) to verify the fetched document; a mismatch fails closed and the manifest is never returned or used. One Python caveat: the fetched bytes are written tocache_dirbefore the digest check, so clear the cache after a mismatch (or keepchecksumon every read, includingoffline=Trueones); a later cache read without it would return the rejected document. - Pinning: request the raw source bytes (
includeSource: truein JS) and compute a deterministicsha256:<hex>pin withmanifestPinDigest, so a later verify can prove it ran against the exact document that provisioned the node. The CLI’s state file uses this pin. - Cache and offline: caching is opt-in. Configure it (
cachein JS,cache_dirin Python) and a network fetch stores the raw document for offline mode to reuse; configure nothing and nothing is stored, so offline mode has nothing to read. The document is re-validated against the schema on every read.
apiVersion, onFailure, allowedCommands). The JS SDK keeps them camelCase; the Python SDK maps them onto snake_case dataclass fields (validation.min/max become min_value/max_value).
Resolve inputs
resolveInputs / resolve_inputs merges caller values with the manifest’s declared inputs: it enforces required, applies defaults, type-checks every declared type (string, int, bool, enum, url, hostname, bytesize, array, object), and applies validation rules (values, pattern, min, max). Inputs marked secret: true are flagged and never echoed in errors.
Pre-flight is a gate
runPreflight / run_preflight runs each check and reports passed, blocked, and per-check results. A check with onFailure: block that fails blocks provisioning; onFailure: warn surfaces without blocking. A privileged check that cannot be auto-run is surfaced for the owner, and a surfaced blocking check still blocks the gate (fail closed). provision refuses to start while the gate is blocked.
Provision: manual vs auto mode
provision runs in manual or auto mode (mode is required on provision in both SDKs), and every step also declares its own mode (manual, auto, or both). The manifest decides; the runner honours it.
Sudo is explicit per command (
never / optional / required). The runner never elevates a command the manifest does not mark. In auto mode, a sudo: required command runs only under a sudo grant matched by prefix against the manifest’s allowedCommands; the effective grant is the intersection of the two (the narrower side wins), so an operator can scope down but the grant can never broaden beyond the manifest. A privileged command outside the grant fails closed: it is not run unprivileged.
Step kinds
Prompts, captures, and the machine wallet
Interactive prompts fill frominput (a resolved input), capture (an earlier step’s value), or owner (answered interactively, never auto-filled). Captured values thread by key into later steps, verification, and post-provision. The runner exposes the machine wallet address as run context (context.machineWalletAddress in JS, {{ context.machine_wallet_address }} in Python, supplied via context=); the manifest declares how that maps into any commission or payout field. The runner itself holds no commission semantics.
Owner-action handoffs
Wallet funding, DNS changes, hardware-wallet signing, and SSH access confirmations areowner-action steps: real pause points. The runner emits the interpolated (redacted) instructions and blocks until the owner returns the declared confirmation. A missing handler or a mismatched answer fails the step.
Idempotency and resumability
Each step may declareidempotency: a check command runs first and skips the step when the effect is already present, and rerun policy is safe, unsafe, or requires-confirmation. The confirmation handler differs per SDK: JS prompts via onRerunConfirm and raises StepExecutionError when the handler is missing, while Python calls on_confirm_rerun and, when it is omitted, silently skips a requires-confirmation step whose check passes. Pass the handler in Python if the re-run decision matters.
An interrupted run always leaves a resumable state: provision returns a resume state, attaches one to the failure error, and (optionally) emits it at every step boundary via onStateChange / on_state_change, so a caller can checkpoint durably as the run progresses. The state is JSON-round-trippable and carries a schema version, the completed step ids (keyed by phase/step), a cursor, and the resolved non-secret inputs and captures. A resumed run skips completed steps and restores the non-secret captures.
The boundary callback also fires at the pre-confirmation owner-action pause, with the owner-action excluded: a run killed at a funding or DNS handoff resumes by re-entering that handoff, never by skipping it.
rerun policy is safe, takes a caller-re-supplied value via resumeSecrets / resume_secrets otherwise, and fails with ResumeNotPossibleError (naming the step and the missing keys) when neither applies. A step never interpolates a missing or empty secret.
Verification closes the loop
verifyProvisioning / verify_provisioning runs the manifest’s probes. Success requires every probe in verification.success.allOf to pass; exit-zero installs alone never count. A privileged probe that cannot be elevated is not run unprivileged: it is reported as failed. ensureVerified in JS/TS, and the result’s own raise_if_failed() in Python, raises VerificationFailedError (listing the failed probes) unless verification succeeded; report the node live only when it does not raise.
Post-provision operations
runPostProvision / run_post_provision runs one opt-in operator utility by id (tail logs, check balance, and similar), honouring its mode, sudo policy, and actor. These are helpers only, never part of the success gate.
Secrets and redaction
A run-scoped redaction registry holds every secret input value plus the declared extraction pattern of every secret capture, registered before its step runs. Every streamed chunk, event, captured output, rendered-file echo, and error message passes through redaction before it leaves the SDK. A secret stdout/stderr capture with no pattern suppresses raw stream emission entirely. Typed errors carry only structural metadata (ids, paths, exit codes), never secret material.Errors
Related
- Monetize function: where provisioning fits in the flow
- Heartbeat SDK reference: report presence once provisioned
- Monetization opt-in API

