Kk-carrier docs

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

Three deliverables on one CDN: the bootstrap verifies installer 1.4.1, which installs product 2.8.0 One CDN, three artifacts https://downloads.example.com/my-service/ install.sh installers/1.4.1/linux-x64/installer installers/1.4.1/manifest.json releases/2.8.0/linux-x64/service releases/2.8.0/manifest.json Illustrative paths, not a K schema. Versioned artifacts stay immutable. Bootstrapinstall.sh, install.ps1 Installer 1.4.1K + adapter, own version Product 2.8.0version = targetVersion verifies hash and size, launches resolves URL, SHA-256, size itself Consults no product release authority. Pins one installer. Not the application version. Not in K's request or receipt. Success only after the local receipt and live readback.
Authenticate both sets of metadata; hashes alone do not establish publisher identity.
DeliverableResponsibility
Bootstrap (install.sh, install.ps1)Identify the platform; select, download, verify and launch a compatible installer; pass the request; supervise settlement; clean temporary code
InstallerK plus your product adapter, released with its own version, platform artifacts, hashes and signatures where available
ProductThe application executable that the adapter's ReleaseSource selects; its version is the adapter's Release.version and the request's targetVersion

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:

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:

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.

FormDelivered artifactRuntime requirement
Single executable per OS and architectureNode SEA with the runner embedded, built with --cjsNo external Node for the worker; supervisor and controller dependencies are separate
Cross-platform JavaScriptK and adapter bundled into one .mjsIndependently 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 single executable, process.execPath is the runner itself, so spawning a script re-runs the runner Under external Node Workernode runner.mjs Controllernode controller.mjs process.execPath = /usr/bin/node spawning execPath + a script runs the script Inside a SEA Worker./runner (SEA) Controllernever reached process.execPath = ./runner spawning execPath + a script re-runs the runner; the upgrade fails at the first controller call Fix: a native or SEA controller, or an explicit interpreter path baked in at build time.

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)?
Control plane and data plane: the release platform is trusted for metadata, the CDN only serves bytes that K verifies Adapter's ReleaseSourcecheckForUpdate()fetchRelease(version) Release platformcontrol plane, authenticatedversion, url, sha256, size CDNdata plane, needs no trustbytes at whatever URL trusts metadata verifies bytes Can be one host playing both roles, an object store, or a data: URL. Authenticate the platform: TLS identity, a manifest signature, or an authenticated API. K's hash check proves only that the bytes match what the metadata claimed.

Common shapes, from simplest up:

ShapeWhat answers the two questionsNotes
Static manifest on a CDNstaticManifestSource({ baseUrl }) reads a JSON manifest you publish with each releaseNo server; rollout policy is whatever you write into the manifest
Your own release APIA small adapter mapping your API's response to a ReleaseAuthentication, cohort selection and reporting are yours to build
HandsK'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.