Skip to main content
Provider provisioning ships in @peaqos/peaq-os-sdk 0.5.0+ (JS/TS) and peaq-os-sdk 0.5.0+ (Python). It is machine-local: the runner executes entirely on the machine, against no external API.
The provisioning runner turns a (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.

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 is client.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 version or resolve "latest"; the concrete version is read back from the document and recorded for reproducibility.
  • Integrity: pass an expected digest (expectedDigest in JS, checksum in 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 to cache_dir before the digest check, so clear the cache after a mismatch (or keep checksum on every read, including offline=True ones); a later cache read without it would return the rejected document.
  • Pinning: request the raw source bytes (includeSource: true in JS) and compute a deterministic sha256:<hex> pin with manifestPinDigest, 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 (cache in JS, cache_dir in 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.
Manifest YAML keys are camelCase (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, 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.
The sudo grant is a scoping and audit aid, not a security boundary. It reduces the blast radius of an honest manifest, but it cannot contain a hostile one: a granted program can still run arbitrary code through its own arguments. Auto mode elevates commands unattended, so only run a manifest you trust. Verify its integrity (fetch with an expected digest) before an auto run, and prefer OS-level isolation for defense in depth. Treat auto mode on an unverified, remotely served manifest as remote root execution.

Step kinds

Prompts, captures, and the machine wallet

Interactive prompts fill from input (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, where context.machineWallet is only a legacy alias slated for removal; {{ 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 are owner-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 declare idempotency: 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.
Secret captures are never persisted, in plaintext or otherwise; only their key names are recorded. On resume, if a remaining step needs a secret from an already-completed producer, the runner re-runs that producer when its 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.
That is automatic inside provision, but not everywhere. verifyProvisioning / verify_provisioning (both SDKs) and JS runPreflight take their own optional redaction registry and do not seed one from your resolved inputs: call them without it and a probe command that interpolates a secret is emitted, executed, and returned unredacted. Python’s run_preflight does seed redaction from the inputs you pass, so hand it your resolved inputs. Pass the registry yourself in the other cases, or run these through the CLI, which seeds it for you.

Errors