@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_sparksystemd service through@push.rocks/smartdaemon. - Runs in an explicit mode:
cloudlyorcoreflow-node. - Activates Docker Swarm through
@apiclient.xyz/dockerwhen daemon mode starts. - Collects host metrics for CPU, memory, disk, and Docker container counts.
- Sends Cloudly heartbeats when
cloudlyUrl,nodeId, andnodeTokenare configured. - Sends authenticated Swarm observation v2 reports after accepted Cloudly heartbeats.
- Schedules recurring tasks with
@push.rocks/taskbufferfor Spark updates and managed Docker service updates. - Optionally schedules host package updates when
--enableHostUpdates=truehas 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:
- 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.
- 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/sparkexecutable, the CLI link, unit files, the mask, enablement and node data, and it never reloads, unmasks, enables or starts a service. - 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.
- 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 unitsmartdaemon_spark-containerd.servicethat 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. - 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,
enabledordisabled, 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. - 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-serveverifies the bundle'spallet-containerd/release, starts Pallet's own containerd on/run/pallet/containerd/containerd.sockwith 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 CloudlysystemUpgrade,reboot, andserveZoneServiceUpdatenode actions when node credentials are configured. Spark executes an action only after Cloudly accepts itsrunningclaim, 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:
- Inside the Pallet enrollment child, the only Pallet process that serves its control socket, reconfirm both current local identities.
- 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. - 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-conversionmoves 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 asspark-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-authorityconverts 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 ofSparkCoreflowOwnerManager.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 asspark-coreflow-owner-authority-ambiguousor-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. |
License and Legal Information
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.