Skip to content

Node Manifest Envelope v0.2.0

This document is the normative description of the Node Manifest envelope for schemaVersion 0.2.0. The envelope is the smallest stable structure that identifies a manifest, binds it to a node, and defines when it is intended to apply. Everything below the envelope is intentionally left unspecified in this version.

The JSON Schema at schema/node-manifest.v0.schema.json is the machine-readable description of the envelope's shape. This document defines the meaning and expected behaviour of the fields exposed there. If there is any ambiguity in how to interpret a field, this text is definitive.

Object shape and extensibility

A Node Manifest in schemaVersion 0.2.0 is a single JSON object. It always carries the fields schemaVersion, kind, manifestId, nodeId, and issuedAt. It may also carry a validity object. Any other top-level fields are considered extension or future content.

Producers are allowed to add additional top-level fields beyond those defined here. Consumers must ignore any top-level fields they do not understand rather than rejecting the document. Consumers may propagate unknown fields unchanged when rewriting or relaying a manifest, but they must not attach any semantics to them unless they explicitly implement the relevant extension.

This rule gives us a stable core envelope while still allowing experimentation around it.

Field semantics

schemaVersion

The schemaVersion field declares which manifest schema this document conforms to. For this envelope it is always the literal string 0.2.0.

Producers emitting manifests that follow this specification must set schemaVersion to 0.2.0. Consumers that are implementing only this version of the specification must treat any manifest whose schemaVersion is not exactly 0.2.0 as out of scope; they may reject it or route it to a different handler, but they must not silently treat it as if it followed this envelope.

The schema version is independent from the repository version in package.json. Repository versioning reflects the evolution of this project as a whole; schemaVersion is the contract identifier that runtimes and external systems rely on when parsing and enforcing manifests.

kind

The kind field is a simple discriminator for the document type. In this repository, schemaVersion 0.2.0 defines one kind: node-manifest.

Producers must set kind to node-manifest for all manifests described by this spec. Consumers must only apply Node Manifest semantics to documents whose kind is exactly node-manifest. Other kinds may be introduced in future schema versions (for example, a receipts document), but those are out of scope here.

manifestId

The manifestId field is a globally unique identifier for this manifest instance. It is intended to be stable for the life of the document and to serve as the primary key that receipts, logs, and external systems use when referring back to "the manifest that was in force".

Producers are responsible for minting manifestId values such that they never reuse the same identifier for two different manifest bodies, even across time and deployments. A simple and recommended pattern is to embed both the nodeId and a timestamp in the identifier, for example using a URN that encodes the platform and an issuance time, but the schema intentionally treats manifestId as an opaque string.

Consumers must not attempt to infer structure from manifestId. They may index on it, correlate receipts against it, and present it in user interfaces, but they must treat it as an opaque token. If a consumer ever encounters two different manifest documents with the same manifestId but different envelope fields, it must treat this as an error in the producing system; this specification does not define how to reconcile such a situation.

nodeId

The nodeId field binds the manifest to a specific platform or logical node.

Producers must set nodeId to a stable identifier for the platform that is supposed to execute this manifest. The precise naming scheme is outside the scope of this spec; it may be a tail number, a hull number, a URN, or an internal registry identifier. What matters is that the same physical or logical node uses the same nodeId across manifests so that runtimes and back-end systems can answer questions like "which manifest was in force for this node at a given time".

Consumers must only apply a manifest to a node whose local identity matches the manifest's nodeId. A runtime running on a platform is expected to know its own nodeId by configuration. If it receives a manifest whose nodeId does not match its own, it must not adopt that manifest for execution.

Nothing in this specification prevents different organisations from using different nodeId schemes, but for any given deployment there should be a single authority that defines and manages the node identity registry.

issuedAt

The issuedAt field records when the manifest was compiled by the upstream planning or commander system. It is a timestamp, expressed as an RFC 3339 date-time string in UTC.

Producers must set issuedAt to the time at which the manifest's content was finalised and considered ready for distribution. It is not the time at which the manifest happened to arrive on a given platform. Producers should use a monotonically increasing clock for issuedAt values and must not deliberately back-date manifests.

Consumers use issuedAt as the tie-breaker when multiple manifests are potentially applicable to the same node at the same time. Given two candidate manifests for the same nodeId, both within their validity windows, the one with the later issuedAt is considered the newer plan and should supersede the older one.

All comparisons are done in UTC. Implementations may accept any RFC 3339 legal value in the payload, but emitting code in this repository should always normalise to the ...Z form.

validity

The validity field, when present, is an object that expresses an explicit time window in which the manifest may be applied, together with a small grace interval for clock skew and late transitions. It refines the more general notion of "newer manifests override older manifests" by giving producers a way to define when a manifest should become active and when it must be considered expired.

The validity object may contain notBefore, notAfter, and graceSeconds. At least one of notBefore or notAfter should be present when a validity object is emitted. If a producer emits a validity object in which both notBefore and notAfter are absent, consumers must treat the manifest as if the validity field were absent and fall back to the default behaviour defined below.

notBefore

The notBefore field, when present, is an RFC 3339 UTC timestamp that declares the earliest instant at which a runtime is allowed to adopt and enforce this manifest.

Before notBefore, the manifest is considered not yet valid. A runtime may receive and store the manifest before notBefore and may use that information for planning or pre-staging, but it must not treat it as the active manifest for execution until the current time has reached or passed notBefore.

If notBefore is absent and validity is present, consumers treat the manifest as eligible for activation immediately, subject to its issuedAt and notAfter values.

notAfter

The notAfter field, when present, is an RFC 3339 UTC timestamp that declares the instant after which this manifest must no longer be treated as valid, except for the limited extension provided by graceSeconds.

A runtime must treat the manifest as expired once the current time is strictly later than the effective expiry time. The effective expiry time is defined as the notAfter timestamp plus graceSeconds seconds if graceSeconds is present and greater than zero, or exactly notAfter if graceSeconds is absent or zero.

If notAfter is absent but notBefore is present, the manifest becomes valid at notBefore and remains valid until superseded by a newer manifest for the same node. If both notBefore and notAfter are absent, consumers may ignore the validity object and fall back to the default behaviour defined below.

Producers must not emit manifests where notAfter is earlier than notBefore. If a consumer encounters such a manifest it must treat it as invalid and must not adopt it for execution.

graceSeconds

The graceSeconds field, when present, is a non-negative integer that extends the expiry moment defined by notAfter to allow for modest clock skew and transition jitter in the system.

Producers may set graceSeconds when they know that there will be some variability in when manifests are delivered and adopted, and they want to avoid sharp edges where a manifest is considered invalid on one node slightly earlier than on another due to clock differences. Typical values are on the order of tens or hundreds of seconds, not hours.

Consumers must never treat graceSeconds as an excuse to ignore expiry entirely. It is purely an extension on top of notAfter. If notAfter is absent, graceSeconds has no effect.

Default validity when validity is absent

If the validity field is completely absent, the manifest is considered eligible for activation immediately at issuedAt and remains valid until it is superseded by a newer manifest for the same nodeId. The selection rules below define what "superseded" means in detail.

Producers are free to omit validity for simple scenarios, such as short-lived test flights. For production use, it is strongly recommended to include an explicit validity window.

Manifest selection rules

This section defines how a runtime chooses which manifest is "in force" for a node at a given moment, when it may have multiple manifests stored locally.

For any node, a runtime maintains a set of known manifests whose nodeId matches its own. At any given instant t (in UTC), the runtime computes the subset of those manifests that are eligible. A manifest is eligible at time t if and only if all the following conditions hold:

It has schemaVersion equal to 0.2.0 and kind equal to node-manifest. Its nodeId matches the node's identity. If it has a validity.notBefore field, then t is at or after that instant. If it has a validity.notAfter field, then t is strictly before the effective expiry time defined earlier. If it has no validity field at all, then t is at or after its issuedAt.

If the eligible set is empty, the runtime must treat the node as having no active manifest. The behaviour in that case is deployment-specific; typical patterns are to fail closed, to fall back to a safe default mode, or to continue executing a previously active manifest until an operator intervenes. This repository will define recommended behaviour in later documents; for now the only normative requirement is that the runtime must not silently invent a manifest.

If the eligible set contains one manifest, that manifest is in force.

If the eligible set contains more than one manifest, the runtime must choose the one with the latest issuedAt timestamp. In the rare case where two eligible manifests for the same node have exactly the same issuedAt value, the runtime must choose one deterministically; a simple and acceptable rule is to choose the manifest whose manifestId is lexicographically greatest. Producers should avoid emitting such ambiguous pairs.

Whenever a new manifest arrives, the runtime re-evaluates the selection rules. If the new manifest is eligible at the current time and has a later issuedAt than the manifest currently in force, it supersedes the old manifest. If it is not yet eligible, it may be stored for future activation. If it is already expired according to its validity window, the runtime may discard it immediately.

These rules are designed so that different consumers with the same set of manifests and the same clock view will make the same decision about which manifest is in force at any given instant.

Unknown top-level fields

As noted above, producers may add additional top-level fields to the manifest object and consumers must ignore fields they do not understand. This is deliberately restated here because it interacts with selection and validity.

Unknown fields must not influence whether a manifest is considered eligible, nor which manifest is selected among eligible candidates. Selection must depend only on the envelope fields defined in this document. This ensures that extension experiments cannot accidentally perturb the core behaviour of runtimes that do not implement those extensions.

The TypeScript binding exposes a helper called selectActiveManifestForNode that implements these rules exactly. Runtimes in TypeScript or JavaScript can treat that function as the reference implementation for envelope selection.