Integrating K
Build an independent installer from K and a trusted product adapter. The application exposes lifecycle and health controls; it does not run K's transaction engine. Read how an upgrade works first, then start with the runnable example. The design states the obligations; the reference has protocols, exit codes and file layout.
Publish three deliverables
| Deliverable | Responsibility |
|---|---|
Bootstrap (install.sh, install.ps1) | Identify the platform; select, download, verify and launch a compatible installer; pass the request; supervise settlement; clean temporary code |
| Installer | K plus your product adapter, released with its own version, platform artifacts, hashes and signatures where available |
| Product | The application executable that the adapter's ReleaseSource selects; its version is the adapter's Release.version and the request's targetVersion |
- The bootstrap must not contain another swap/rollback algorithm. K provides
launchRunnerfor Node callers; there is not yet a product-ready shell bootstrap template. install.shandself upgradeuse the same runner protocol and installation state. Keep versioned artifacts immutable.- What each entry does on each machine, and what it owes the caller for each outcome, is the installer contract; step 4 builds it.
- The controller is an execution role, not a fourth deliverable.
1. Define the adapter and state
createRunner requires a HostAdapter. Supply release lookup, installation ownership, consent policy, notification handling and lifecycle operations through trusted build-time code. Use checkCompatibility(from, to) for transitions constrained by data or protocol compatibility. Another package manager's installation is managed-elsewhere.
The controller implements the six operations of the host control contract: fence, quiesce, stop, start, healthProbe and resume, either directly or through createCommandHost. Test fence against your real service manager. The obligations apply only to workloads your product promises to preserve across an upgrade; a stateless service acknowledges quiesce and resume.
Choose one persistent stateDir per installation for slots, journal and receipts. Keep application data, installer scratch code and interpreter outside the slots. Run the installer outside the application's service-management boundary: spawning a child does not escape a systemd cgroup or Windows job.
2. Establish the initial installation
K's upgrade flow requires trusted, usable bytes in stable. For an existing installation, a trusted setup step calls bootstrapStable({stateDir, version, artifactPath}) with its current executable. This seeds a fallback; it does not authenticate or download those bytes or start a service. It refuses conflicting state and does not overwrite initialized stable. Fresh installation, adoption of an existing installation and repair of state K cannot settle are installer work; see repair beyond K.
The controller starts the K-selected artifact via slotArtifactPath or the artifactPath that createCommandHost passes to it. Each slot contains one artifact.bin; package layouts and additional install hooks need a product contract.
Promotion renames the slot directories, so a Windows controller must copy or hard-link the artifact to a runtime path outside the slots before starting it. Copying is a good default on every platform: it keeps the runtime path stable across promotion. The example copies the selected artifact to active.mjs (Node also needs the extension).
K restores executables, not data migrations. Keep repair and cleanup limited to owned installation state and provide backup and restore for destructive data changes.
3. Build and launch
On the build machine, bundle your trusted adapter with K:
pnpm install --frozen-lockfile
node scripts/build-runner.mjs examples/external-service/adapter.ts /tmp/k-runner.mjs
Follow the example setup before invoking that example runner. After authenticating the caller and obtaining approval for the target, submit a request from an operator shell or independent supervisor:
printf '%s' '{"protocolVersion":1,"action":"upgrade","id":"install-2","targetVersion":"2.0.0","consented":true}' | node /tmp/k-runner.mjs
consented records approval; it is not authorization supplied by an untrusted network client. Requests cannot select adapter modules, commands or release URLs. Logs go to stderr and the response to stdout.
Directly invoking a worker does not supervise it. Use launchRunner for a supervised install, or superviseRunner when the caller needs a structured result. Both enforce execution and recovery deadlines and recover only the original operation. The example installer wires this flow; see packaging for shipping the runner as a single executable.
4. Implement the entries
Step 3 submits one request by hand. A product has three entries that do it for the user, and the installer contract says what each should do on each machine, attended or not. It is advisory; what to build from it:
- Decide presence once. An explicit
CIor*_NON_INTERACTIVE=1wins; otherwise a terminal. Everything that asks keys off it; an unattended run never asks, and running it is the consent. - Make every entry produce the same request. An id, an exact version and recorded consent. A channel is resolved to a version before anything changes; attended, the person sees it and answers. Nothing selects code, commands or URLs.
- Settle before anything else. Every entry runs recovery first and reports what it settled. Only then does it read the world it is in: fresh and adopted are
bootstrapStableand a self-check; managed is the upgrade; broken, records that cannot be settled or read, is reinstalled over in the same run, with the old state quarantined. - Ask the product two things after reading the world and before stopping anything: is a service running, and what must the user still do. What was running comes back; what was not is not started. Not set up and attended, a fresh install runs the product's first setup on the terminal.
- Print one line and one exit code per outcome. What happened, what did not change, what to do next, in the user's words. The outcomes table gives the line, and what attended and unattended runs add.
The reference implementation of this step is raft-computer-installer: a bootstrap, a self upgrade command and a remote entry on top of one K runner. There is not yet a product-neutral template.
5. Observe and recover
Inspect both the operation outcome and the exit code; the reference lists every code. Retry the same id and target to replay a terminal result; use a new id for a new attempt. Recovery itself is described in the guide under when something goes wrong. What the integration has to provide:
- Exit 3 from the supervisor leaves a verified runner and
recovery.json. An installer built to the contract treats this as broken and reinstalls; a caller of K alone can keep that directory and callresumeRunner(path)to retry offline. - If the whole invocation dies, something must run a compatible installer against the same state again: your product's OS startup hook or an operator. K does not install a watchdog.
- If a request died before its operation was recorded, recovery refuses rather than guessing. Inspect
statusand explicitly runrecoveron the retained runner. - Never clear a lock or receipt merely to bypass unresolved work.
Repair beyond K
Of the five worlds, K's transaction owns the managed and the upgrading ones. Fresh and adopted are bootstrapStable, start and probe, as in step 2. Broken needs repair, which is installer work: the installer contract describes it and the repair boundary states what it must keep.
6. Validate the product
Use the test plan, then test your real installer and controller on each target platform. Cover baseline setup, running-service upgrade, bad-candidate rollback, installer death, offline recovery, workload and data retention, and service isolation. Observe declared OS lifecycle surfaces before retiring their previous manager. A green framework test is not product acceptance.
Package the installer
End users download finished artifacts; these build choices belong to publishers. scripts/build-runner.mjs produces both bundle forms; SEA injection, signing and publication are publisher responsibilities. Whichever form you ship must survive stopping and replacing the application.
| Form | Delivered artifact | Runtime requirement |
|---|---|---|
| Single executable per OS and architecture | Node SEA with the runner embedded, built with --cjs | No external Node for the worker; supervisor and controller dependencies are separate |
| Cross-platform JavaScript | K and adapter bundled into one .mjs | Independently available Node 24 and adapter dependencies |
One JS file is portable only if its adapter and dependencies support the targets.
Build a single executable
Node's single-executable-application (SEA) tooling embeds the runner into a copy of the Node binary. This recipe uses Node 24 and a CommonJS entry. The supervisor executes the result directly, with no interpreter option.
The SEA pitfall. Inside a SEA, process.execPath is the SEA itself, so spawning it with a script re-runs the runner. Make the controller a native executable or its own SEA, or bake an explicit interpreter path into the adapter. The example adapter spawns its controller with process.execPath, so as published it only works under an external Node; the recipe below was verified end to end with that command changed to an explicit Node path.
# 1. CommonJS entry. Point the adapter's controller command at an explicit
# Node path first (see the SEA pitfall above).
node scripts/build-runner.mjs --cjs examples/external-service/adapter.ts dist/runner.cjs
# 2. Prepare the blob.
printf '%s' '{"main":"dist/runner.cjs","output":"dist/sea-prep.blob","disableExperimentalSEAWarning":true}' > dist/sea-config.json
node --experimental-sea-config dist/sea-config.json
# 3. Inject into a Node binary for the target platform.
cp "$(command -v node)" dist/runner
# macOS only: codesign --remove-signature dist/runner
npx postject@1.0.0-alpha.6 dist/runner NODE_SEA_BLOB dist/sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
# macOS: add --macho-segment-name NODE_SEA to the postject command
# macOS only: codesign --sign - dist/runner (use your release identity)
# 4. Publish dist/runner with its sha256 and size; launch it with no interpreter.
Build one SEA per target OS and architecture with that platform's Node binary. Binary size depends on the target Node build. Installer and product release metadata can include optional gzip transport; both paths use the same verified downloader.
This packages the worker only. launchRunner is a Node API, and the demo controller also needs Node. To ship an installation chain that needs no preinstalled runtime, package the supervisor and controller dependencies too.
Release platforms
K has no opinion about where releases come from. The adapter's ReleaseSource answers two questions, and anything that can answer them is a release platform as far as K is concerned:
checkForUpdate()- Which version should this machine move to, if any? Channels, cohorts, staged rollouts, pinning and version ordering all live behind this call.
fetchRelease(version)- For exactly this version, what are the URL, SHA-256 and size (plus optional gzip metadata)?
Common shapes, from simplest up:
| Shape | What answers the two questions | Notes |
|---|---|---|
| Static manifest on a CDN | staticManifestSource({ baseUrl }) reads a JSON manifest you publish with each release | No server; rollout policy is whatever you write into the manifest |
| Your own release API | A small adapter mapping your API's response to a Release | Authentication, cohort selection and reporting are yours to build |
| Hands | K's sibling project: channels, staged rollouts and in-app update metadata (source) | The adapter maps a Hands response to a Release. Not a requirement for publishing or bootstrapping the installer |
Whatever the platform, K has no built-in connector or result uploader for it. Authentication, channel policy and remote reporting belong to the integration; forward the actual operation id and outcome, since publication is not installation success. Withdrawing a release affects future distribution only; it does not roll back installed machines.