jkunz d6ec1562c2
CI / Type Check & Lint (push) Successful in 2m13s
CI / Build Test (Current Platform) (push) Successful in 2m38s
CI / Build All Platforms (push) Successful in 15m1s
Verify journaled release / verify-release (push) Failing after 39m49s
v34.0.1
2026-09-27 13:43:27 +00:00
…
…
…
…
…
…
…
…
…
…

@serve.zone/spark

Spark is a Deno-powered server management agent for serve.zone hosts. It installs as a system daemon, activates Docker Swarm, schedules host/service maintenance tasks, and provides the bootstrap profiles currently used by Cloudly and Coreflow node deployments.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Current Role

Spark is intentionally small and operational. It is not a general-purpose configuration management framework; it is the serve.zone node-side utility that knows how to run itself as a daemon and keep selected Docker services moving.

The current implementation:

  • Installs and updates a smartdaemon_spark systemd service through @push.rocks/smartdaemon.
  • Runs in an explicit mode: cloudly or coreflow-node.
  • Activates Docker Swarm through @apiclient.xyz/docker when daemon mode starts.
  • Collects host metrics for CPU, memory, disk, and Docker container counts.
  • Sends Cloudly heartbeats when cloudlyUrl, nodeId, and nodeToken are configured.
  • Sends authenticated Swarm observation v2 reports after accepted Cloudly heartbeats.
  • Schedules recurring tasks with @push.rocks/taskbuffer for Spark updates and managed Docker service updates.
  • Optionally schedules host package updates when --enableHostUpdates=true has been configured.

Spark also owns a callable Swarm observation v2 producer. Every Swarm member captures a bounded, coherent local container and physical-mount census between matching before/after container sets. Active managers additionally capture the same bounded /services?status=true, /tasks, and /nodes view, including canonical service-spec, constraint, inventory, and mount-correlation identities. Incomplete or changing census branches are omitted instead of claiming evidence that Docker did not provide. Worker observations never invent the manager-only Swarm cluster ID. Heartbeat transport owns a separate private reporter session, validates bound Cloudly receipts, retries only the exact pending request after ambiguous or retryable delivery, and quarantines terminally rejected sessions until process restart.

Installation

A serve.zone node runs the complete Linux node bundle. Install it from the release's own installer with one exact version:

curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | sudo bash -s -- --node-bundle --version <version>

That mode is described under Complete node bootstrap; a host that still runs the released Swarm daemon is converted in the order under Cutover to the node runtime.

Released standalone executable

The default installer mode installs the released Swarm-era executable and nothing else. It remains the line for a host that has not been converted yet, and it installs no node bundle, no node service and no enrolled identity.

Install a released binary:

curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | sudo bash

Pin to the newest release within a supported major channel:

curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | sudo bash -s -- --major 1

Verify a selected release asset without changing the host:

curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | bash -s -- --version <version> --verify-only

The installer downloads SHA256SUMS.txt and verifies the exact platform binary before stopping services or replacing an existing installation. Release builds currently target Linux x64/ARM64, macOS x64/ARM64, and Windows x64.

On systemd hosts, upgrades preserve smartdaemon_spark.service and its enablement. Only a previously active daemon is stopped and restarted; inactive or failed daemons remain stopped. An existing noncanonical spark.service, uncertain service state, or a command symlink pointing at another installation stops the installer before binary replacement. Resolve that ownership explicitly before retrying; the installer does not delete or adopt another service.

The verified executable is staged on the installation filesystem and renamed over the old binary. Other installation contents and configuration are left alone. An existing executable is retained at the backup path printed during installation. A failed restart returns a nonzero exit status with the new binary still installed; inspect the daemon before choosing recovery. Automatic binary rollback is not attempted because a started version may have migrated state. A completed bare installation prints the node-bootstrap order for the version it just installed; the released line has no daemon-installation command to advertise.

Complete node bootstrap

The node integration after 1.12.0 adds --node-bundle and installnodebundle; use a verified release containing these commands. On a Linux systemd host, run the release's installer with one exact version:

curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | sudo bash -s -- --node-bundle --version <version>

The transition from 32.0.1 must use the qualified 32.1.1 installer with the exact 32.1.1 version in both the download URL and --version. Spark 32.0.1 requires the legacy manifest digest in the editable release body, so its installed binary cannot resolve a bodyless journaled release itself. The 32.1.1 installer verifies the standalone 32.1.1 binary and uses that binary to fetch and verify the complete bundle. The 32.1.0 tag and frozen journal bytes are retained, but its release is a permanently withdrawn draft because its compiled payload included checkout-local dependency state; never install or resume it. Later journaled releases use the normal bundle client and do not repeat the private direct-upload exception used to recover 32.0.1.

This mode runs the verified standalone executable from disposable staging to download, verify and select the complete native bundle at /opt/spark/current. It preserves the old /opt/spark/spark executable, global CLI link, unit files, mask, enablement and node data. Existing root-owned 0755 code roots retain their mode; new roots and owned release/seal directories use 0700.

The canonical service must be masked and quiescent, or absent, with no alternate spark.service, drop-ins or stale manager configuration. If NeedDaemonReload is set, explicitly reload systemd and re-prove this admission before retrying. Bootstrap never reloads, unmasks, enables or starts a service. It requires a pinned version and rejects --major, --install-dir and --verify-only.

A verified external executable can also run installnodebundle X.Y.Z directly. The SPARK_NODE_BUNDLE result records the selected version, commit and manifest digest. Retry uses that exact release and rejects any different current selection; normal updates use the separate drain/activation owner. Successful bootstrap means code is selected. Credential enrollment through enrollnode and definition installation through installnodeservice remain explicit steps, and production activation requires the separate rollout acceptance.

Cutover to the node runtime

A host that still runs the released Swarm daemon is converted in this order, one step at a time, each step verified before the next is run:

systemctl mask --now smartdaemon_spark.service
curl -fsSL https://code.foss.global/serve.zone/spark/releases/download/<version>/install.sh | sudo bash -s -- --node-bundle --version <version>
/opt/spark/current/spark enrollnode   # {"operation":"adopt-existing","hostname":"<hostname>"} on stdin
/opt/spark/current/spark installnodeservice
systemctl unmask smartdaemon_spark.service
/opt/spark/current/spark activateguard
systemctl start smartdaemon_spark.service

What each step proves, and what it never does:

  1. Mask and stop. The operator's own step, and the only one before the start that changes what runs. It leaves the released daemon unable to be started by anything else while the host converts, which the Spark steps below require: bootstrap and adoption admit this host's canonical unit only masked and quiescent, service installation requires it quiescent, and guard activation admits no active Spark runtime. Docker services on the host keep running; masking Spark removes nothing it created.
  2. Bootstrap the bundle. Proves that the exact release's complete native bundle is verified and selected at /opt/spark/current, recorded by version, source commit and manifest digest. It preserves the old /opt/spark/spark executable, the CLI link, unit files, the mask, enablement and node data, and it never reloads, unmasks, enables or starts a service.
  3. Adopt the existing node. Proves that this host holds its own current Spark and Pallet credentials in its protected stores. It requires the canonical unit masked and fully stopped, reads the released configuration file exactly once through the versioned migration, and leaves that file's bytes unchanged. It changes no service enablement and starts no old runtime. Adoption enrolls a new Spark credential generation for the same node, after which the released daemon's old credential is rejected.
  4. Install the node service. Proves that the units which will boot this host are exactly the canonical definitions, by digest: the node unit that executes /opt/spark/current/spark runnode, and Pallet's containerd unit smartdaemon_spark-containerd.service that executes /opt/spark/current/pallet/pallet-control containerd-serve, requires and starts after the guard unit, and is wanted by and ordered before the node unit. It preserves masks, enablement and previous generated definitions, and it never starts, stops, enables or unmasks anything.
  5. Unmask, then activate the guard. The unmask is the operator's own and starts nothing; it is what makes the just-installed node unit admissible to activation. Activation admits that unit only loaded, enabled or disabled, fully stopped, without drop-ins or a pending reload, and requiring and ordering itself after the guard — a masked unit is none of these. An already enabled unit may start on reboot once unmasked, so do not leave a reboot gap here. Activation then proves that the retained allocation-pool guard is commissioned, its boot unit installed and enabled, and both dependency edges to networkd present. An active Spark runtime, an alternate network manager, a pending provider reload or an administrator drop-in prevents activation. It publishes no positive network barrier and releases no allocation.
  6. Start. The operator's last step, and the second one that changes what runs. Systemd selects the installed local-vendor definition; verify the exact process and its readiness immediately. Starting the node unit first starts the containerd unit it wants: Pallet's containerd-serve verifies the bundle's pallet-containerd/ release, starts Pallet's own containerd on /run/pallet/containerd/containerd.sock with Pallet's complete configuration, imports the bundled pause image and reports ready only once the CRI runtime answers; the node runtime is its only client. Docker and its own containerd keep running beside it, untouched — Pallet refuses their sockets by path and by file identity. The first start reads the node's runtime routing binding from Cloudly, which answers only once the node's cluster relay is live, so convert a host only after that. See Foreground node runtime.

None of the Spark commands above stops, removes or adopts a running service, and none of them deletes the released configuration source. The three systemctl steps are the operator's own, and only the mask and the start change what runs.

A host converted under Spark 33 reinstalls its units. Spark 34 adds Pallet's containerd unit and changes the node unit's bytes (it now wants and starts after that unit). Until installnodeservice runs again, such a host's installed node unit is not the canonical definition, so its boot identity reads as unconverted and asdaemon no longer refuses by name. After selecting the Spark 34 bundle, stop the node unit and rerun /opt/spark/current/spark installnodeservice, then activateguard and the start as above. A host enrolled fresh under Spark 34 needs nothing extra.

After the cutover, spark asdaemon refuses to start on this host by name (released_mode_retired). The refusal is decided from this host's own boot path — a verified bundle together with an installed unit whose bytes are the canonical runnode definition — and it happens before the released configuration is written and before any managed service is pushed, so no Coreflow owner session is opened and no Docker secret is created. Nothing running is stopped or removed by the refusal: the command only declines to own the host.

The released configuration source is retired separately, and only after adoption has consumed its credential. The node's own record states it retained-inert at generation one and removed at generation two, after the exact imported bytes are unlinked; a replay advances nothing and a removed source can never become a retained one. See Store migrations; no command invokes that retirement yet.

The relay credential is not Spark's to deliver. The released owning mode wrote the Coreflow relay's launch environment as a Docker secret containing JUMPCODE. The current Coreflow relay authenticates with SERVEZONE_CLUSTER_RELAY_AUTHORIZATION read from its launch environment and reads no JUMPCODE. Spark never holds that bearer and delivers none, so the cluster relay is redeployed out of band, with that variable in its launch environment, by whoever holds the credential. Converting a node neither moves, restarts nor revokes a relay running on it.

Cloudly is upgraded first, always. Publishing a Spark release is the fleet rollout: a node with a running released daemon checks for a newer release every ten minutes and installs it, with no channel pin and no per-node gate (updateSpark under Daemon Behavior). Central Cloudly must therefore consume the @serve.zone/interfaces release a Spark build requires, and be deployed, before that Spark release is published. Spark offers its two session kinds under separate minimums for exactly this reason, and the Swarm minimum is raised only in the release that starts requiring the newer Cloudly; a Cloudly that is older then refuses the Swarm offer for every self-updating node at once. See Protocol handshake.

Requirements

  • Linux with systemd for daemon operation.
  • Docker for service and Swarm management.
  • Root privileges for daemon installation, Docker maintenance, and package updates.
  • Deno only when running from source.

macOS and Windows binaries are built for CLI/library availability, but the operational daemon paths are Linux/systemd oriented.

Quick Start

The commands in this section are the released Swarm-era line and apply to a host that has not been converted. On a converted host spark asdaemon refuses by name; see Cutover to the node runtime.

An unconverted host already carries the smartdaemon_spark.service unit written by the release that installed it. spark claimjump is the only command that still writes one, as part of Cloudly Jump setup; there is no separate daemon-installation command.

Run daemon mode with a profile:

sudo spark asdaemon --mode cloudly

or:

sudo spark asdaemon --mode coreflow-node

The selected mode is persisted in a user-home npmextra key/value store under the servezone_spark identity. Later spark asdaemon calls can reuse the stored mode when no --mode flag is provided.

Follow daemon logs:

sudo spark logs

CLI Reference

spark <command> [options]
Command Purpose
claimjump --claimUrl <url> --jumpCode <code> Claim a Cloudly Jump code, persist the returned node/Coreflow credentials, and install/start the daemon. Also accepts CLAIM_URL and JUMP_CODE.
enrollnode Enroll separate Spark and Pallet credentials from one JSON document on stdin, using an installed verified node bundle. Does not activate the daemon.
runnode Run the selected compiled node bundle in the foreground using existing enrolled identity and settings.
installnodeservice Install the canonical systemd definition for the verified current node bundle, preserving masks and enablement.
activateguard Commission or recover the retained allocation-pool guard, install its boot unit, and verify persistent networkd dependencies.
guard-recover Recover the existing retained guard from the selected bundle; the systemd boot entry uses this command.
upgrade Install a newer Spark release when available and restart the daemon.
asdaemon --mode cloudly Run the daemon loop with the Cloudly profile. Refused by name on a host whose boot path is the node runtime.
asdaemon --mode coreflow-node Run the daemon loop with the Coreflow node profile. Refused by name on a host whose boot path is the node runtime.
metrics Print a JSON host metrics snapshot.
logs Follow journalctl -u smartdaemon_spark -f.

claimjump, upgrade and asdaemon remain in the binary for hosts that have not been converted. They are no longer the advertised path: a completed bare installation prints the node-bootstrap order instead.

installdaemon, updatedaemon and prune were removed. Both daemon writers wrote a unit whose ExecStart is spark asdaemon under the canonical name the node runtime's own runnode unit uses, so on a converted host they would clobber that unit with a process that now refuses to start; neither is part of the conversion order, which masks the released unit before the bundle is installed. prune was a destructive Docker sweep outside the ownership protocol with no place in the node runtime.

Pallet credential enrollment

spark enrollnode is the external credential bootstrap for a verified, selected Linux node bundle under /opt/spark. The bare-binary installer above does not provide that bundle. Run the command as root with one JSON document on stdin followed by EOF; it accepts no command options, credential environment variables, executable paths, or caller-supplied writer-stop claims. Input is limited to 16 KiB and must finish within 30 seconds.

For an existing node, the input is:

{"operation":"adopt-existing","hostname":"worker.example"}

The command requires smartdaemon_spark.service to be masked and fully stopped, with no pending job, main/control PID, or remaining cgroup. A noncanonical spark.service must be absent. It verifies that state before the one-time import, again before enrollment, and after cleanup. It reads only the canonical root daemon's /root/.npmextra/kv/servezone_spark.json; the versioned migration preserves the source bytes and records the original identity and digest in private NoSQLDB state. The command never changes service enablement or starts an old runtime.

A fresh node instead uses this input, supplying the actual Jump code through stdin:

{"operation":"jump","cloudlyOrigin":"https://cloudly.example","hostname":"worker.example","bearer":"<16-character Jump code>"}

Fresh enrollment also permits an absent canonical Spark unit. Cloudly communication uses only the configured HTTPS origin. Spark and Pallet generate and retain separate credentials in their own protected NoSQLDB stores; Spark receives no Pallet bearer. Explicit provisioning creates missing platform data parents, while ordinary resume only recreates disposable IPC directories. Enrollment needs no cluster relay: the Pallet runtime routing binding is read and bound on the node runtime's first start (see Foreground node runtime).

Resume a persisted attempt without repeating credentials or reading the legacy file:

{"operation":"resume"}

After the coordinator confirms both current local identities and joins the Pallet child and Spark database cleanup, stdout contains one SPARK_ENROLLMENT record followed by JSON with credentialEnrollment: "complete", runtimeReady: false, and a secret-free enrollment summary. Other dependency lifecycle log lines can precede that record. This result does not establish workload or remote-session readiness. A lost response or outcome_unconfirmed must be reconciled through the saved attempt; no second identity or legacy bootstrap is substituted. If failure preceded initial store provisioning, repeat the exact original input to provision it.

Daemon Behavior

Spark.daemonStart() starts the node reporting and reconciliation subsystems before enabling recurring tasks. A failed initial managed-service reconciliation stops already-started subsystems in reverse order instead of leaving a partial daemon running. The asdaemon command handles SIGINT and SIGTERM by disabling actions, terminating active action process groups, stopping tasks and service reconciliation, releasing the Coreflow session, and stopping heartbeat reporting before exit. Shutdown aborts active pinned image pulls and retains the Coreflow owner session until reconciliation settles. A pull that ignores cancellation retains the owner session until that reconciliation eventually settles. Separately, subprocess cleanup that cannot prove exit retains exact process tracking and quarantines the affected lifecycle restart while the daemon continues its ordered shutdown.

  • SparkServicesManager.start() activates Docker Swarm and, for Coreflow nodes, acquires the host-wide reconciliation lock.
  • SparkTaskManager.start() schedules recurring maintenance tasks.
  • SparkHeartbeatManager.start() sends node metrics to Cloudly when node credentials are configured.
  • SparkMetricsStreamer.start() streams lightweight metrics and, after initial service reconciliation completes, executes Cloudly systemUpgrade, reboot, and serveZoneServiceUpdate node actions when node credentials are configured. Spark executes an action only after Cloudly accepts its running claim, requires Cloudly to accept the terminal result, reports non-owner service-update skips as failures, and disables further actions if terminalization cannot be proven.

Heartbeat metric branches, host updates, self-updates, and action processes are cancelled and drained during shutdown. Metric commands are timeout-bounded and escalate from SIGTERM to SIGKILL; action process groups, including detached descendants, must terminate before lifecycle restart is allowed.

Scheduled tasks:

Task Schedule Action
updateServices Daily at midnight in cloudly mode Checks generic managed Docker services for image and published-port drift. Coreflow-node updates are initial-startup and Cloudly-action driven with complete authority-bound shape proof.
updateSpark Every 10 minutes Checks for a newer Spark release and reloads the daemon after upgrade.
updateHost Daily at midnight when enabled Runs apt update/upgrade/autoremove/autoclean.

The managed service list is populated by the selected mode before daemon startup. Service updates use version-pinned Docker image pulls, Docker secrets, and declared published port mappings.

Base service images

serve.zone images publish released version tags only, so Spark states the exact version of every base service it installs. binary/base-service-images.json pins each one by registryHost, repository, version and the repository digest that version resolved to; this release installs Coreflow 32.2.0 and Cloudly 32.0.0. Upgrading a cluster to a newer base service is an explicit change of that manifest and a new Spark release, never a moving tag.

A managed-service descriptor is refused by name when it leaves the installed version open: a reference without a tag, a mutable tag such as latest, a digest in place of the version, or a digest that is not a canonical repository digest. The pinned tag is pulled and verified against the pinned digest, so a registry that moved the tag fails the pull instead of silently installing another build, and the created Swarm service carries the digest-pinned reference (code.foss.global/serve.zone/coreflow@sha256:...).

Reconciliation therefore decides on that digest: a running service is replaced when its image is not exactly the pinned digest — including a service still bound to the version tag or to a different digest of the same repository. The image version label is descriptive only. A Coreflow service an earlier Spark created from a mutable tag is still adopted, because adoption proves ownership of the pinned repository and the following reconciliation moves it onto the pinned digest.

In coreflow-node mode, Spark records the durable Coreflow owner in Swarm Spec labels through version-index CAS. The matching node label is only the scheduling projection. Coreflow is configured for one replica on that manager with no published ports, the Docker socket and /var/lib/serve.zone/coreflow mounted writable, and a root-owned secret.json mounted with mode 0400. Its szncorechat network must be a non-ingress, non-internal, non-attachable, IPv4-only Swarm overlay with the exact coreflow alias. Coreflow may add at most one alias-free sznwebgateway attachment and one read-only /sys/fs/cgroup:/host/cgroup mount. New services are created at zero replicas, proven against the complete managed shape, and only then scaled to one desired replica. The state directory and spark-owner.json marker are root-owned with modes 0700 and 0600. The retained host lock at /var/lib/serve.zone/.spark-coreflow-owner.lock is also root-owned with mode 0600. Spark does not migrate ownership automatically when the recorded node disappears or loses manager status. Within managed reconciliation, only the current owner may create, stop, remove, or replace the Coreflow service and secret. Each owner Spark process uses a new opaque Coreflow config revision, so an owner daemon restart rotates the Coreflow secret and replaces the service once. A wrong same-name szncorechat network fails closed instead of being replaced.

Protocol handshake

Spark states the @serve.zone/interfaces release it was built against on every body it posts to Cloudly, and accepts any peer from 32.0.0 on. It opens two session kinds, because it runs two runtimes rather than two versions of one:

Session kind Runtime Routes
sparkNode pallet node (runnode) POST /spark/nodes/heartbeat
sparkSwarmNode Swarm-era coreflow-node daemon POST /spark/swarm-nodes/{heartbeat,metrics-sample,action-result,swarm-observation}

These routes carry no typed error, so Cloudly answers a refused offer with HTTP 422 and an IProtocolRefusal body, judged before the node is authenticated. Spark tears nothing down when that happens: it keeps its credentials, its bundle, its timers and any pending Swarm observation, logs the refusal once, and offers again after the shared five-minute refusal cadence. The node runtime reports the state protocol-incompatible while it holds one. An accepted answer clears the refusal and returns the host to its configured interval. A refused node action is left with Cloudly, which still holds it and delivers it again, rather than disabling this node's actions.

Cloudly heartbeat requires a Spark node token. spark claimjump is the Spark-side claim path for Cloudly Jump setup: it claims the Jump code over HTTP, validates the response, persists cloudlyUrl, the Coreflow jumpcode, nodeId, and nodeToken, and installs the daemon. Existing installations without those values continue to run service maintenance without reporting node telemetry.

Programmatic Usage

Spark exports Spark and SparkSwarmObservationManager from mod.ts:

import { Spark } from './mod.ts';

const spark = new Spark();
await spark.daemonStart();

// Stop timers, action processes, tasks, reconciliation, and heartbeat reporting.
await spark.daemonStop();

The public instance exposes:

Property Purpose
smartdaemon systemd service integration.
sparkConfig persisted mode/config key-value store.
sparkTaskManager taskbuffer scheduler and built-in maintenance tasks.
sparkUpdateManager Docker Swarm activation and managed service update logic.
sparkMetricsCollector Host metrics collection for local output and Cloudly heartbeat payloads.
sparkHeartbeatManager Cloudly heartbeat and authenticated Swarm observation client.
sparkMetricsStreamer Cloudly metrics stream and guarded node-action executor.
sparkSelfUpdater Bounded binary and checksum download/update lifecycle.
sparkSwarmObservationManager Callable, serialized read-only Swarm observation producer.
coreflowConfigRevision Per-process opaque revision used for owner-managed Coreflow secret rotation.
sparkInfo package metadata lookup.

Collect one local observation without using the heartbeat transport session:

const observation = await spark.sparkSwarmObservationManager.collectObservation();

Direct SparkSwarmObservationManager construction requires an IDockerSwarmReadClient; the optional reporter session, initial sequence, and clock inputs support lifecycle ownership and deterministic tests. Collection accepts an optional AbortSignal, bounds Docker reads, and keeps timestamps strictly increasing within its reporter session.

Development

Run from source:

deno task dev

Quality and build tasks:

deno task check
deno task lint
deno task fmt
deno task test
deno task compile

The package scripts intentionally defer to Deno tasks; pnpm build only reports that no Node build is needed.

The source checkout pins pnpm and disables install lifecycle scripts through pnpm-workspace.yaml. Its npm wrapper postinstall downloads an already published Spark binary, so running it during source release preparation would depend on the release being built. This source-only setting is not included in the npm wrapper. Frozen Deno commands install the runtime imports, including TsPack and TsDeno, from deno.json and deno.lock.

The lock is registry-neutral: its npm entries carry no tarball URL. Deno writes one only when it resolved a package from a registry other than npmjs, and from Deno 2.9.7 it refuses a lock whose tarball origin is not the configured registry. The tracked .npmrc therefore sets registry=https://registry.npmjs.org/; Deno and pnpm both read it from the project root and prefer it over ~/.npmrc, so a lock refreshed on a machine whose home configuration names a mirror still resolves from npmjs. Deno's NPM_CONFIG_REGISTRY environment variable overrides the project .npmrc: the graph resolution shared by the release build and the notice generator refuses to run while it names another registry, and refuses a lock that carries any tarball field.

Release tags use GitZone's journaled Gitea-assets target. The release build runs with the pinned Deno compiler (2.9.7, denoVersion in binary/node-build.json, which must be the compiler of the pinned Pallet control executable) and scripts/build-node.ts --release --gitea-manifest, which creates both Linux node archives, the five standalone CLI assets, the installer and legal files in one verified TsPack set. Its strict schema-1 attachment manifest also includes the TsPack manifest and checksum list. All five executables are compiled from one owner-verified private DENO_DIR with the frozen lock, --node-modules-dir=none and --exclude-unused-npm; checkout node_modules is never a release input. Every compile runs through @git.zone/tsdeno (pinned in deno.json), which leaves out npm files the target provably cannot load and prints a size report. Each executable's compile target in .smartconfig.json (@git.zone/tsdeno) also drops NoSQLDB's packaged engines and debug server and UI, which Spark never loads because it starts NoSQLDB only with the bundle's verified engine; deno task compile builds the same targets. The same section pins the Deno release (denoVersion, which the release build requires to equal the node recipe's) and gives each executable a size floor and budget and a smoke check. TsDeno refuses any other Deno, fails a binary below its floor (Deno 2.9 can exit 0 with a fraction of the binary when npm downloads fail) or above its budget, and on the host's own target runs it once as spark --version in an empty environment, where it must exit 0 and print its version; binaries for other targets skip the smoke check. The release build hands TsDeno its own running compiler by path. With Deno 2.9.7 the Linux executables are about 280 MB (439 MB before the trim, with Deno 2.9.4) and the macOS and Windows ones 232–242 MB (from about 410 MB). Notice generation resolves that same isolated graph, and the exact sorted npm package identities and their digest are recorded in the notices and versioned build receipts. The builder requires those inventories to match before compilation, rechecks the graph, source commit and lock after all targets finish, and removes the private dependency cache before TsPack seals the artifacts. GitZone freezes those exact bytes in its schema-4 release journal before Git or Gitea mutation, publishes them through the configured target, and verifies every public attachment anonymously. If publication is interrupted after the journal is written, gitzone release resume reuses the retained bytes without rebuilding. The tag workflow has no publication credentials or build authority; it verifies the completed public release, its source commit, full asset set, checksums and both Linux architectures. It polls for at most one hour only while the release is absent or its canonical attachment set is still incomplete, permits at most three consecutive transport failures, and rejects a complete invalid release immediately.

Spark's documents are owned by @lossless.org/client/nosqldb, the persistence client that vendors the SmartData document implementation; the compiled qualification report states the exact upstream baseline it was built from.

On Linux, deno task test also compiles and runs an isolated NoSQLDB identity-engine qualification. It uses a verified published native executable and disposable SmartData records to check exact engine selection, exclusive ownership and transaction recovery after engine termination. It also recovers the new private Spark identity store's pending and acknowledged credentials after separate engine terminations. This is a migration prerequisite; the Spark daemon still uses its existing configuration store. A successful local test does not qualify production cutover, host power-loss recovery or another CPU architecture.

The backend-private ts/identity/ module durably prepares Spark-only credentials, binds an exact outbound request digest, and activates only an exactly matching acknowledgement. Immutable receipts support replay without restoring superseded credentials. The module is not wired into the daemon or exported as an enrollment API: daemon startup integration, configuration activation, sidecar installation/self-update and the atomic Cloudly enrollment exchange are still required. Pallet owns a separate store and credential.

Private enrollment helpers now consume the published Interfaces 32.18.0 contract. prepareAdoption requires imported Spark authority and authenticated Cloudly state; it records the source fingerprint, original proof hash and exact generation/session fence without activating either credential. The caller must authenticate that state over HTTPS using the exact imported bearer. Generic prepare/bind/acknowledge calls cannot bypass this adoption path. Shared binding checks Spark's own prepared hash against the complete Spark/Pallet proposal, and activation requires an exact shared acknowledgement. The immutable receipt retains adoption provenance.

readEnrollmentRecovery is a private coordination capability, not a status or IPC API. It returns only Spark's bearer for the exact bound pending proposal or its still-current acknowledged identity, allowing recovery across partial activation. Changed proposals, unbound material and superseded credentials are rejected. It never returns Pallet's bearer or the original imported proof.

The private ts/enrollment/ coordinator now persists an immutable operation intent before external I/O, followed by the complete prepared proposal, both-owner binding, authenticated Cloudly acknowledgement and confirmed local completion. Restart resumes that same intent and credentials. A failed initial exchange permits one protocol-defined pending-Spark recovery request; an unconfirmed result stays bound and never creates replacement credentials. Saved acknowledgements bypass network enrollment and resume the independent Pallet-then-Spark activation.

Its owner ports are private integration boundaries. Status contains only stage, enrollment ID, Cloudly origin and node ID. Completed enrollment is historical installation evidence, not proof of current remote authority or workload health. Source tests use a real native Spark store and test-only external-owner stubs; the compiled fixture also recovers bound, acknowledged and complete coordination state after native engine termination. Daemon integration remains required before production cutover.

SparkEnrollmentHttpTransport implements the Cloudly port using a fixed canonical HTTPS origin and TypedRequest 8.0.4. Both RPCs use exact published contracts; no redirect, cache, implicit retry or stream transport is enabled. One request deadline covers response consumption, request/response bodies are capped at 32/16 KiB, and cancellation drains the actual fetch/body owner before returning. Credential RPC hooks are disabled and errors omit remote text, URLs, nested causes and arbitrary abort reasons. Shared response validation still proves exact node/proposal binding. Native TLS tests use a disposable CA and server certificate; production uses normal certificate verification. The adapter is not yet connected to daemon startup, and Cloudly's new enrollment handlers are still not exposed.

SparkEnrollmentPalletTransport implements the local owner port against published Interfaces 32.18.0 requests.pallet methods. It accepts trusted installer metadata, defaults to UID 0 and /run/serve.zone/pallet/control.sock, and requires a protected mode-0700 parent and mode-0600 socket. It rejects symlinks, unsafe ancestors and changed socket/parent inodes before sending enrollment data. No database access, plaintext Pallet bearer, TCP fallback or permissions repair is available.

Each TypedRequest uses one bounded length-prefixed JSON frame followed by request write-half-close, with 32/16 KiB request/response caps. The client requires the full reply and EOF, strict UTF-8, matching envelope identity and shared proposal/phase/ assigned-node bindings. Hooks, implicit retries, caches and stream transports are disabled. One default 10-second deadline aborts socket I/O; the actual connection closure and pending path checks remain joined before return. Once a peer has half-closed its request, the server still needs its own deadline to bound a lost client while no response is being written. Endpoint protection assumes trusted root/installer ownership, not protection against a hostile root process.

Real Unix tests cover all three methods, Jump/adoption bindings, malformed frames, endpoint ownership/replacement, pre-await input capture, cancellation, deadlines and caller-directed lost-response replay. Both adapters remain private and are not yet connected to the running daemon or production bootstrap.

SparkIdentityRuntime owns the private native database lifecycle using NoSQLDB 10.5.1 fileStorageStartup: 'current-format-only'. It verifies the selected engine digest and protected paths; NoSQLDB admits current layouts read-only and acquires its native file-root lease before storage writes. No Spark advisory lock or local format detector remains. Each start uses a separate mode-0700 disposable IPC directory so a rejected competitor cannot remove the active owner's socket.

Shutdown drains accepted callbacks, closes SmartData, then stops the engine. A failed database close retains the native lease for cleanup retry. Startup and shutdown deadlines bound caller waits without abandoning actual work or allowing restart before cleanup settles. Ordinary startup refuses missing storage instead of creating an empty replacement; explicit first installation alone provisions it. Strict legacy/mixed/staging rejection is tested without storage mutation. The compiled test uses this owner directly and verifies the exact 10.5.1 static musl engine provenance; production bootstrap wiring remains separate.

test/native/qualify-identity-upgrade.ts qualifies a disposable store written by the NoSQLDB 10.2.0 engine that Spark 33 shipped through 10.5.1 startup, writes and a second restart. Supply the two published static musl engine paths in that order; the fixture checks their independently pinned SHA-256 values. It preserves active identity, an unfinished rotation and settings. Downgrade requires a pre-upgrade backup.

test/native/qualify-released-store-readthrough.ts qualifies one document implementation against another over a store the other one wrote. Run it as record <seededStoreRoot> <dumpPath> on the implementation that owns the store's bytes and as verify <seededStoreRoot> <dumpPath> on the implementation under qualification; the dump is the canonical byte image of every document the released-store conversion leaves behind. The supplied store is consumed, because the engine binds a store to the path it was created under: seed a fresh store at the same path for each run. The ledger contributes ids only, since its applied timestamp is wall-clock.

SparkPalletProcess owns one verified foreground Pallet control process through Deno's native process API. Trusted bundle metadata supplies its protected absolute path and SHA-256. A bounded file verifier permits the larger compiled control while preserving the separate 256 MiB NoSQLDB engine limit. Its default enrollment mode selects enrollment-serve or explicitly requested enrollment-provision. Trusted mode: 'runtime' selects runtime-serve with pallet.node.process.v1 readiness and rejects provisioning; the two modes use separate child lifetimes.

The owner admits its scoped callback after Pallet's versioned readiness event. Parent cancellation, invalid process output or child exit closes admission and cancels the callback's signal; the callback must drain its admitted work before returning. Stdin carries only the parent lifetime. Shutdown closes it for graceful EOF, then escalates to SIGTERM/SIGKILL after bounded grace periods if needed. The owner waits for actual OS exit and output drainage; unconfirmed exit retains the lifetime instead of permitting a restart. Forced termination is a failure, and the owner never starts a replacement child. Runtime shutdown allows 330 seconds by default for the native operation window and cleanup, versus 15 seconds for enrollment. Runtime output is drained with bounded chunks and 4 KiB stdout lines; enrollment additionally retains its 1 MiB cumulative output limit. Static errors prevent child diagnostics from becoming public error text.

deno test --allow-all test/palletprocess.test.ts

The focused tests cover protected artifact validation, readiness, provisioning selection, cancellation, callback failure, malformed output, startup timeout, forced cleanup and runtime log drainage beyond the enrollment lifetime limit. The node recipe pins the published Pallet 34.0.2 release and its NoSQLDB 10.5.1 engine. Before compiling Spark, packaging verifies its control record, source provenance and Pallet's own containerd CRI socket /run/pallet/containerd/containerd.sock (a build naming the host's shared /run/containerd/containerd.sock, which on a Docker CE host is Docker's daemon, is refused), and the exact 95-file control inventory: 11 native roles, 24 clock files, the five pallet-containerd/ files (containerd, its runc shim, runc, the pause image archive and their manifest, whose digest the control record carries as containerdManifestSha256), 38 SmartDNS notices, 3 CoreDNS notices, and 14 SmartVPN notices. Every declared file must match the sealed archive's size, hash, and mode. The whole upstream legal tree is included under pallet/: third-party-notices.md indexes it, native-notices/ holds the executor's generated native notices (every file named by native-notices/manifest.json), and notices/containerd/ and sources/containerd/ hold the containerd distribution's license texts and corresponding sources, which notices/containerd/manifest.json lists completely. Packaging and notice generation both refuse a missing, changed or unlisted file in either inventory. SmartNFTables 2.6.0's guard binary and build provenance are independently pinned for each architecture, and both verify its complete tsrust.native-notices inventory under pallet/notices/smartnftables/ against the pinned manifest digest. Installation requires the runtime, guard, DNS, VPN, clock and containerd executables with their provenance and notice manifests. It admits the sealed empty pallet-clock/gnutls.config and rejects undeclared executables. Runtime supervision is not yet wired into the external service activation flow or self-updating. runnode composes it with the ordinary local lifetime described below. The released asdaemon command still uses the legacy service managers on a host that has not been converted, and refuses to start once this host's boot path is the node runtime.

Foreground node runtime

Run the selected compiled executable with runnode and no additional arguments. The command verifies the installed whole bundle and requires its selected Spark path to match the running executable. A bare download, stale selected version, or deno run mod.ts runnode cannot start node ownership.

/opt/spark/current/spark runnode

SparkNodeRuntime reopens existing Spark identity and settings and requires a complete persisted enrollment. Startup then runs in a fixed order:

  1. Inside the Pallet enrollment child, the only Pallet process that serves its control socket, reconfirm both current local identities.
  2. In the same child, prove that Pallet holds this node's runtime routing binding. A binding Pallet already holds is read back offline, so a bound node restarts without Cloudly. A node without one reads its current binding from Cloudly (getNodeRuntimeBinding, authenticated with the node's Spark credential) and binds it once.
  3. Join the enrollment child, then start the Pallet runtime, which refuses to start without a binding.

Cloudly answers the binding only once the node's cluster relay is live. A refused or unreachable answer, an answer that does not bind to this node, and a binding Pallet refuses each stop startup by name (runtime_binding_unavailable, runtime_binding_invalid, runtime_binding_conflict), with no retry, no direct-to-Cloudly fallback and no runtime start. Pallet holds the binding immutably: a moved relay needs a rebind capability that Pallet and the interfaces contract do not offer yet. Missing state or settings fail; ordinary startup never provisions replacement data, imports legacy configuration or resumes an incomplete remote enrollment. Only disposable IPC parents are recreated.

The command emits SPARK_NODE records with protocol spark.node.process. The ready event reports the selected bundle, secret-free enrollment summary, settings snapshot, localRuntimeReady: true and workloadReadiness: "unverified". SIGINT or SIGTERM closes admission, joins reporting and any host update, and waits for Pallet and Spark storage cleanup before emitting stopped. Failed or forced cleanup remains an error even when a stop signal initiated shutdown. Loss of either owner cancels admitted work; no replacement process starts within the same lifetime.

After local readiness, the runtime collects CPU, memory and root-filesystem observations directly from the host and sends the node heartbeat to POST /spark/nodes/heartbeat at the exact enrolled HTTPS origin. Every attempt reads the current private Spark credential and requires a receipt for that node and credential generation. Requests and responses are limited to 8192 and 2048 bytes; the ten-second deadline includes reading the response. Redirects, cookies, response caching and immediate retries are disabled. Completed attempts wait the persisted heartbeatIntervalMs before the next sample. Cloudly unavailability does not block local readiness; a receipt records accepted host observations and does not establish workload readiness or deliver operator actions. A Cloudly that refuses this build's protocol offer is reported as protocol-incompatible and re-offered on the refusal cadence, without dropping local readiness. Diagnostic state changes go to stderr and contain no credentials or remote response text.

When the persisted hostUpdatesEnabled setting is true, a separate joined owner runs /usr/bin/apt-get update, upgrade -y, autoremove -y and autoclean -y at the next host-local midnight, in that order and without overlapping runs. There is no immediate startup run or replay of missed offline schedules. A failed command stops that day's sequence. Cancellation terminates and joins the process group, allowing up to five minutes before forced termination; an unproven cleanup remains a runtime shutdown error. This owner does not replace the selected Spark bundle. Settings are read once per runtime lifetime, so changed settings take effect after a controlled drain and restart.

runnode now wraps the runtime in SparkNodeService. It holds a private Unix HTTP control socket at /run/serve.zone/spark-node/control.sock, with a root-owned mode-0700 parent, mode-0600 socket and retained empty IPC lock anchor. This process lock is acquired before starting data owners and remains held after they drain. A competing service cannot replace the listener or start another runtime. Stale socket retirement requires the lock, a refused connection and matching inode and ownership checks. Native HTTP owns final socket cleanup; privileged operators must not replace its pathname while the service is running.

SparkNodeControlClient reads bounded spark.node.control status containing the actual PID, random process-instance ID, phase and exact bundle version, source and manifest. A drain command must match the observed instance. It closes admission and reports draining until both data owners have joined; only then does it report drained. The process and control socket remain alive until SIGINT/SIGTERM, allowing an external installer to prove drainage before stopping the systemd unit. Repeated drains of that instance are idempotent. Losing the caller does not cancel an accepted drain, and failed cleanup never produces a successful drained result. No readiness, settings, credentials or other application data are written to files.

SparkNodeServiceActivation composes this client with the existing systemd activation policy inside SparkBundleInstallation.run. It matches the old PID and selected manifest before drainage, then verifies the new PID and exact selected bundle after start. Private HTTP requests have a five-second bound; startup observation allows 90 seconds and drainage 360 seconds. These are observation limits; a timed-out caller cannot undo or abandon server-side drainage. This path preserves masks and enablement and rejects the alternate spark.service unit.

The same activation owns Pallet's containerd unit across a bundle change. Before any effect it admits that unit only absent (a host whose units predate it) or loaded with the service settings the contract requires, as the service manager reports them with drop-ins included (unsafe_containerd otherwise), and a running containerd-serve only when its own executable is the pallet/pallet-control of a release of this installation. After the node unit has drained and stopped and the new bundle is selected, a running containerd whose release carries a different pallet-containerd/manifest.json is stopped and started again, so it runs the new selection's containerd, shim and runc, before the node unit starts; the restarted process must run from the new release with the same contract. KillMode=process and Delegate=yes keep every shim and container running across that restart, and the new containerd reattaches them. An unchanged runtime keeps running, and a stopped containerd is left for the node unit to start. The result states containerdRestarted.

Stable systemd boot entry

After installing and selecting a complete node bundle, configure its service with:

/opt/spark/current/spark installnodeservice

The command takes no additional arguments and requires root, a verified current bundle and a quiescent canonical service. It holds the same native installation lock used by bundle selection. SmartDaemon 2.8.0 installs /usr/local/lib/systemd/system/smartdaemon_spark.service with direct execution of /opt/spark/current/spark runnode, Type=exec, KillMode=mixed, a 360-second stop deadline and Restart=no. The atomic current selection is the stable boot path; the node unit explicitly requires and starts after the retained guard unit, so a failed guard also blocks node startup and its database owners.

The same command installs Pallet's containerd unit, /usr/local/lib/systemd/system/smartdaemon_spark-containerd.service, to the contract Pallet's readme states: /opt/spark/current/pallet/pallet-control containerd-serve as the root main process with null stdin, Type=notify with NotifyAccess=all, KillMode=process and Delegate=yes (stopping the unit stops containerd and leaves every shim and workload running), Restart=always with a short RestartSec=, a 60-second stop deadline, LimitNOFILE=infinity, TasksMax=infinity and OOMScoreAdjust=-999, and Requires=/After= the guard unit. The node unit Wants= it and starts after it, so the node runtime, containerd's only CRI client, never starts before Pallet's containerd is ready. Spark remains the systemd main process and verifies its selected executable before opening its data owners. Graceful shutdown signals Spark first, allowing it to join Pallet and storage before the whole-cgroup failure deadline.

Installation preserves existing masks and enablement, retains previous generated definitions and verifies the reloaded manager view. It leaves an absent service disabled. It never starts, stops, enables or unmasks a unit, initializes data, changes settings or constructs the legacy daemon. An active owner, alternate spark.service, administrator shadowing definition or effective drop-in is rejected. A failed reload leaves the new definition for explicit inspection; selection and definitions are never automatically rolled back.

Successful installation emits a SPARK_NODE_SERVICE JSON record with the exact selected bundle, boot path, definition digest and changed/masked flags. Service start and enablement remain explicit operations after enrollment and rollout admission. External bundle activation supports both control-group and mixed, requires the kill policy to stay unchanged during activation, and still proves private drainage and exact new-PID readiness independently of systemd state.

Production conversion from the legacy service remains gated on rollout admission. Local readiness does not establish a Cloudly session, containerd workloads, private DNS or production takeover.

Retained allocation-pool guard boot entry

After enrollment, while management networking is available, the privileged installer runs /opt/spark/current/spark activateguard. The command accepts no path, provider, policy or credential overrides. Its supported host profile uses the verified systemd-networkd service; an alternate network manager, pending provider reload, administrator drop-in or active Spark runtime prevents activation. An already installed node unit must also be unmasked, loaded and stopped, and require and order itself after the guard; rerun installnodeservice while it is stopped to update an older definition. Qualify the host's actual interface ownership before using this profile.

One bundle installation lock spans the complete operation. Before a guard unit exists, Spark runs the verified Pallet control's network-acquire mode. That projection-only connection retains fresh authenticated authority without starting native owners, accepting assignments or delivering runtime reports. Spark requires the exact pallet.network.acquisition.v1 ready/stopped sequence, drained pipes and successful child exit before explicitly commissioning Pallet's durable guard lane. A failed or cancelled acquisition leaves commissioning and unit installation unstarted. It then installs smartdaemon_spark-guard.service as a disabled Type=oneshot unit with RemainAfterExit=yes, manually starts it, and persistently enables it. The exact definition orders recovery after local filesystems and before network-pre, networkd and shutdown; both network-pre and networkd require it. Success requires the exact file digest and fragment, no drop-ins or pending reload, completed oneshot state, and both forward and reverse dependency edges. Enabled state alone does not establish complete boot protection.

The unit executes /opt/spark/current/spark guard-recover. That launcher verifies the selected whole bundle and its own executable, then runs its authenticated Pallet control with null stdin. It uses read-only bundle inspection so it can run while the activating parent holds the installation lock. The bounded child owner requires pallet.guard.process.v1 ready/stopped events, drained pipes and successful actual exit. Cancellation or forced cleanup fails the boot prerequisite.

An existing exact unit admits offline recovery only and never repeats projection acquisition. Interrupted reload, start or partial enablement is resumed from retained state; missing guard data is never silently recommissioned. Masks and foreign definitions reject without replacement. Errors retain the guard policy and any installed dependencies. Completion emits one SPARK_GUARD record identifying the bundle and definition. This deny-only bootstrap does not publish a positive network barrier, release allocations or establish workload/DNS readiness.

test/helpers/palletenrollment.fixture.ts is an explicit cross-artifact qualification executable. Supply absolute paths for the Node executable, built Pallet control module, exact NoSQLDB engine and its independently verified SHA-256, and the built Cloudly enrollment store module:

deno run --allow-all --frozen test/helpers/palletenrollment.fixture.ts \
  /path/to/node /path/to/pallet/dist_ts/control/classes.enrollmentcontrol.js \
  /path/to/rustdb_linux_amd64_musl VERIFIED_ENGINE_SHA256 \
  /path/to/cloudly/dist_ts/manager.node/classes.nodeenrollmentstore.js

It runs real Pallet and Cloudly owners in separate Node processes and the real Spark coordinator with independent stores. Cloudly's actual enrollment/credential stores and installed TypedRequest router run behind a disposable loopback HTTPS listener; Spark uses its actual HTTPS client with an isolated test CA, not disabled TLS checks. Fresh Jump and imported-node adoption both recover a committed-but-lost TLS response and a lost local activation result. All three owners and their exact NoSQLDB engines then restart from separate file stores and converge on the same node/credentials. Assertions verify one enrollment receipt, two distinct credential authorities, the expected generations, rejection of the old Spark credential, settings continuity, and unchanged synthetic legacy bytes. The Cloudly authority stub has been removed.

This qualifies the private store/transport integration, not the production CloudlyServer route/admission lifecycle, a distributable node bundle, daemon wiring or deployed workload readiness. The isolated fixture registers no production route and introduces no local package link or application dependency on sibling sources.

The private runtime context now includes SparkSettingsStore. It stores only typed hostUpdatesEnabled and heartbeatIntervalMs settings; connection origin, node identity and credentials remain owned by enrollment and the current identity. Ordinary reads reject missing settings. Explicit fresh installation initializes the existing false/60000 defaults, and revision-checked full updates reject stale writers and counter exhaustion. Responses are detached/frozen and contain no credential or migration-source metadata. An ambiguous write must be reconciled by reading the saved revision; it is not automatically repeated.

The settings projection follows the legacy import, copies operational settings once and retains the exact import source digest as completion provenance. Later startup/import replay preserves user edits. Explicit import preflights any fresh-initialized settings before the import may persist authority, and snapshots installer options before those checks. Without an import, startup leaves settings absent until explicit provisioning. Tests cover concurrency, ambiguity, restart and native crash recovery. These are the new owner's APIs, not a KV compatibility adapter: the existing daemon/CLI still uses npmextra until coordinated bootstrap replacement connects the new lifecycle and removes those old consumers.

Store migrations

ts_migration/ owns every conversion of state an earlier Spark release wrote. The runner walks an ordered list of idempotent steps, refuses un-migratable state by name before any step writes, and records each completed step as one row in the spark_migrations collection through the same exact models the rest of the store uses. runSparkMigrations() returns the applied and already-applied step ids plus the outcome of the legacy import.

  • released-store-conversion moves the settings, identity state, identity receipt and enrollment documents to unversioned ids, drops the settings shape version and renames the legacy source and adoption discriminants. It also drops the shape version from the five shared contract payloads those documents embed, and re-derives the node-enrollment digest, whose domain and hashed payload both changed with the release; the stored copies of that digest in the identity state and identity receipts move in the same transaction, while a digest the enrollment does not derive is preserved exactly. It writes the replacements first and removes the released documents second, so an interrupted run is finished by the next one. A migrated id that already holds a different document is refused as spark-migration-unversioned-id-occupied. The bootstrap import row keeps its released id: it is the completion record of the released npmextra import, and released migration history is immutable.
  • coreflow-owner-authority converts the Coreflow owner marker and the Swarm authority labels that state which runtime owns this node. Both lose their shape version; the owner node id and authority id keep their bytes. It runs inside the existing owner fence of SparkCoreflowOwnerManager.ensureAuthority(), because that process owns the Docker Swarm authority and holds no database. The converted marker is its durable completion record. A marker that names a different owner than the Swarm authority, a partial label set, or a marker of unknown shape is refused as spark-coreflow-owner-authority-ambiguous or -unreadable. Services and secrets created by a 1.15.0 owner still carry the retired resource label until the next reconciliation replaces them.

The ts_migration/ runner runs inside that private owner's startup, after protected database readiness and before identity readiness. An explicit installer-only legacyImport option admits the owning user's npmextra JSON file only after the installer has stopped the old writer. The path must be canonical, owned by the expected UID, regular, singly linked, bounded to 1 MiB, and free of symlinks and unsafe writable ancestors. Import never invokes npmextra or repairs permissions. It hashes the original bytes, validates all known settings and stores the configuration, exact original Spark bearer, Coreflow Jump value and completion stage together in one exact SmartData document. Unknown fields, partial credentials and invalid present settings fail closed. The historical file is not modified.

Explicit replay requires the same path and exact bytes. Ordinary startup omits legacyImport and uses protected state without reopening the historical file. Missing optional settings retain the previous daemon defaults; a credential-free source never becomes an enrolled identity. Imported authority is separate from the new private identity store: no generation or acknowledgement is fabricated. The daemon/CLI configuration consumers and the authenticated adoption exchange are still unwired.

Source retirement has an owner in ts/legacy/: one record beside the import in the node's protected store states what this host has done with that source, and nothing else. It is retained-inert at generation one, and removed at generation two after the file's bytes are revalidated against the recorded digest through the same fenced read the import used and then unlinked. The unlink is fenced by its own durable counter, written before the file is touched, so an interruption between the unlink and the transition is repaired by the next call while a first attempt that finds no file refuses by name. Every transition is offered to the contract's own successor rule before it is persisted, so a replay advances nothing and a removed source can never become a retained one. The record holds a digest and two counters, never a member decoded from the file. No command invokes it yet: retirement must occur only after the coordinated Spark and Pallet activation is durable, which is also when adoption has consumed the source's credential.

deno test --allow-all test/bootstrapimport.test.ts
deno test --allow-all test/legacyretirement.test.ts

Source map:

Path Purpose
mod.ts CLI entry point and library export.
ts/spark.cli.ts Command routing.
ts/spark.classes.spark.ts Main class wiring.
ts/spark.classes.updatemanager.ts Docker Swarm and service update manager.
ts/spark.baseserviceimages.ts Base service image pins, refusal rules, and digest references.
binary/base-service-images.json The pinned version and digest of every base service image.
ts/spark.classes.coreflowowner.ts Durable Coreflow ownership, marker, lock, and node projection.
ts/spark.classes.swarmobservationmanager.ts Swarm observation v2 producer.
ts/spark.classes.taskmanager.ts Scheduled maintenance tasks.
ts/spark.classes.config.ts Persisted mode/config store.
ts/identity/ Private SmartData credential state machine; not yet used by the daemon.
ts/enrollment/ Private durable Spark/Pallet enrollment coordinator and typed owner ports.
scripts/compile-all.sh Multi-platform Deno compilation.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
Outbound-connected serve.zone node agent for enrollment, host management, runtime reporting, and maintenance actions.
Readme
51 MiB
v34.0.1
Latest
2026-09-27 13:43:27 +00:00
Languages
TypeScript 98%
Shell 1.4%
JavaScript 0.6%