The standard as a signed, distributable artifact
Normative.
The engine is a runtime. The standard is data it loads. They version separately, ship separately, and update separately.
This is the design that makes "always running the latest standard" achievable without turning every bay into a silent remote-code-execution target. It is worth being precise about why, because the safe version and the dangerous version look similar from a distance.
1. What a bundle is#
An SGA Bundle is a signed archive containing:
- the field catalog — every core field, its unit, sign convention, type, range, and provenance semantics
- device class definitions — what a
sensor.ballis expected to provide - JSON Schemas for every message type
- validation rules — ranges, cross-field constraints, plausibility bounds
- the registered vocabulary — downgrade reasons, lie types, club types, error codes
- capability-to-role mappings used to compute the connection matrix
- a manifest with
bundle_version,min_protocol_version, issue date, and a digest of every other file in the bundle - a detached signature over the manifest, made with the SGA root key
sga-bundle-2026.09.1.tar.gz ├── manifest.json version, protocol floor, date, a digest per file ├── manifest.sig the SGA signature over manifest.json ├── fields/ the catalog ├── schema/ JSON Schema ├── vocab/ enumerations └── rules/ declarative validation
gzip rather than a newer compressor. The engine has to run on Windows, Linux and macOS and stay portable to mobile platforms, and gzip is in the standard library of every runtime that matters to us while zstd generally is not. The code path that verifies a signature is the last place to take on a third-party dependency, and at this size the saving would be tens of kilobytes against a 512 KB target.
The signature covers the manifest, and the manifest covers everything else. Verification is therefore one signature check and a digest per file, and it still holds for a bundle that has been extracted — a bay can prove what it is running years later without keeping the archive it arrived in, which is what section 6 depends on. Signing the archive bytes instead would be marginally simpler and would lose that.
Because the signature is over the manifest's bytes exactly as they appear in the bundle, no canonical form has to be agreed between signer and verifier. That removes the failure mode where the two disagree about canonicalization and a good bundle is refused — or a modified one is not.
A bundle is a build artifact, never hand-edited. It is generated from the field catalog and signed by an automated process triggered by ratification, and it is reproducible from the catalog at that commit. The partner portal, the JSON Schemas, the conformance rules and the bundle all derive from that one input. A hand-maintained bundle beside a hand-maintained specification drifts within two releases, and the person who finds the drift is an implementer who trusted the documentation.
A bundle contains no executable code. No scripts, no plugins, no expressions, no bytecode. Declarative data only, with a schema the engine validates the bundle against before loading it. This is the single property that makes automatic distribution safe, and it MUST NOT be relaxed — the moment a bundle can express "run this," bundle distribution becomes code distribution and everything in §4 stops being true.
2. How a bundle reaches a bay#
Four paths, all of which converge on the same rule.
1. Online refresh. On start, and periodically after, the engine asks the SGA bundle server for the current version. It downloads, verifies the signature, validates against the bundle meta-schema, stages, and swaps atomically. Failure at any step leaves the previous bundle in place.
**2. Carried by any vendor software package — installers and updates. A vendor whose product installs software on the bay host ships two files in every package, not only in the first-run installer: the engine updater and the current standards bundle**. A launch monitor's connector update, a simulator's patch, a facility management client's release — each carries the pair that was current when that package was built.
Both sit in the same folder as the installer when it runs, and each is compared on its own. The installer replaces the engine updater if the copy it carries is newer than the one on the device, and separately replaces the bundle on the same test. Either, both, or neither may move. A vendor shipping an already-current pair moves nothing.
Two files rather than one because they fail in opposite directions. A bay holding a current bundle behind a two-year-old engine cannot read the standard it has; a current engine with a stale bundle does not know the fields that have shipped since. Pairing them is what makes "the bay is current" a single statement rather than two that can drift apart.
This does not weaken §4. The bundle is declarative data and applies silently. The updater is executable and moves only when a person runs a vendor's installer — which is precisely the "ordinary channel, with consent" that §4 requires for a binary, not a silent push from SGA.
This is a certification requirement, not a courtesy — see Governance. It costs a vendor one file in a build artifact, and it converts every participating vendor's existing update channel into an SGA bundle distribution channel. A bay that never sees sga.org still tracks the standard, because the products on it are being updated by someone.
3. Sideload. An operator copies a bundle file onto the machine — thumb drive, network share. Same verification, same rules.
4. Peer. In a facility, one engine with connectivity may serve bundles to its neighbors. Signature verification is unchanged, so a compromised peer can offer nothing a compromised network could not.
The rule, in all four cases: highest verified
bundle_versionwins.
That rule is also the reassurance a vendor's release engineer will want: an update package carrying an older bundle than the one already cached is a safe no-op. A vendor cannot regress a bay by shipping late, and does not need to rebuild a release because the bundle moved the week after they cut it.
A bundle takes effect at a round boundary and nowhere else. A verified bundle that arrives mid-round is cached immediately and held; the engine swaps it in when the round ends. Every session manifest, every shot record and every uploaded round carries the bundle_version in force (see section 5), so a swap mid-round would produce a round whose own record disagrees with itself -- shots scored under one version and a manifest naming another. A bay one release behind for the length of a round is a far smaller problem than a round nobody can re-evaluate afterwards.
Caching it before the boundary rather than at it matters for the same reason everything else here is cached: a verified bundle on disk survives a restart between arrival and the swap.
The requirement is the bundle, never the engine. A vendor whose shipped engine is already current ships only the bundle — a data file, not a binary. Nobody is asked to re-embed a runtime they already delivered.
Size is our problem, not theirs. A bundle is a field catalog, some JSON Schemas and a vocabulary; it compresses to well under a megabyte. The budget is a target of 512 KB compressed and a hard ceiling of 2 MB, and if we ever approach the ceiling that is a signal the bundle has grown something that belongs elsewhere. A delta bundle carrying only what changed since a named prior version is also permitted, and will typically be a few kilobytes.
At that size the update-payload objection disappears: a vendor adds less to their installer than a single icon asset. Keeping it that small is a standing design constraint on SGA, and it is cheaper to hold than to negotiate.
Freshness is bounded rather than absolute. A certified package MUST embed a bundle no more than two releases behind the current one at the time the package is built. Nobody is asked to rebuild a shipped artifact; they are asked not to embed something a year stale.
Packages that install no host software — a pure firmware update to a sealed device, say — are exempt. There is nowhere to write a bundle, and the device gets its field catalog from whatever engine it negotiates with.
That rule is what makes the offline case work. A bay with no internet that has not been touched in two years gets brought current the moment anybody installs any SGA-aware product on it — regardless of which vendor shipped that installer, because every vendor's installer carries the same signed artifact from the same authority. A launch monitor vendor's update path becomes a simulator vendor's update path, for free.
3. Verification#
An engine MUST NOT load a bundle unless all of the following hold:
manifest.sigverifies against a pinned SGA root key compiled into the engine build. Pinned, not fetched: a key an engine can be told about is a key an attacker can tell it about.bundle_versionis greater than the currently cached version. Downgrades are refused. This blocks rollback attacks — serving an old bundle with a known-weak validation rule.min_protocol_versionis satisfied by this engine.- The bundle validates against the meta-schema; every file matches the digest the manifest lists for it; and the archive carries no file the manifest does not list. A file present but unlisted is unsigned content riding inside a signed artifact, which is how a verifier gets used as a delivery mechanism.
- Signature timestamp is within the acceptable window, or the bundle carries a valid long-term offline attestation. Air-gapped bays are a first-class case, not an exception.
Failure at any step: keep the current bundle, log it, surface it to the operator, keep playing. A bundle problem MUST NOT stop a round. An engine that refuses to run because it could not reach the internet is an engine that gets uninstalled.
Fleet visibility. An engine reports its bundle_version and the path it arrived by in the session manifest and on upload. Aggregated, that tells us how stale the installed base actually is and which vendors' update channels are propagating — which is the difference between believing the distribution model works and knowing it does.
Retention is not optional. Every published bundle stays retrievable permanently and immutably -- see section 6. A verification step that can only check against the current bundle cannot answer anything about an old record.
Key rotation ships in bundles, signed by the outgoing key, with an overlap window measured in years. Engines pin the root, so a root compromise needs a client update — which is exactly the property we want, because a root compromise should be hard to recover from quietly.
4. Why data auto-updates and code does not#
This is the important distinction, and it is worth stating plainly because the two look alike.
| signed bundle | signed binary | |
|---|---|---|
| contains | declarative data | executable code |
| worst case if SGA's signing key is stolen | attacker changes a validation range or adds a bogus field; engine still cannot be made to execute anything | attacker executes arbitrary code on every certified bay in the country |
| reviewable by a vendor before trusting | yes, it is a readable archive | not really |
| needs elevated privilege to apply | no | usually yes |
| appropriate to apply silently | yes | no |
So: bundles update automatically and silently. Engine binaries update through the ordinary channel the platform provides, with consent.
That preserves the whole benefit of the auto-update idea — a bay always speaking the current standard, offline bays repaired by any installer — while keeping SGA out of the business of being a silent software distribution channel into thousands of third-party commercial machines. If SGA ever becomes that channel, SGA's signing key becomes one of the more attractive targets in consumer sports technology, and the blast radius of losing it is every bay we have ever certified.
There is also a plain commercial consequence: a vendor's security review will approve embedding a component that loads signed declarative data. Many will refuse to embed a component that silently self-updates executable code from a third-party server. The safe design is also the one that gets adopted.
4a. Where consent for an engine update comes from#
"With consent" above is not a gesture. It names a specific, recorded act, and this section says which one.
An unclaimed bay never updates its own engine. Nobody has agreed to anything, and there is no account in which an agreement could be recorded. A bay in that state is updated the way any other software is: somebody runs an installer.
Claiming a bay is the consent point. The claim already exists, it is already an authenticated act by the person responsible for the machine, and it already carries the Bay Owner Terms — so it is the natural place to ask. The claim flow asks whether the SGA Engine on that bay may update itself, and the answer is stored on the account, not on the bay, per bay, changeable afterwards wherever claimed bays are listed.
On the account rather than on the bay for three reasons: it survives a reinstall or a replaced host; it is visible to the owner in one place rather than needing a walk around a facility; and it cannot be flipped by whoever happens to be standing at the machine.
A build with self-update compiled out ignores the setting, and reports that it did. A vendor embedding the engine may remove the capability entirely, which is the answer to the security review in §4: the component cannot self-update, and the build says so rather than asking anyone to take it on trust.
Reporting versions is not consent, and does not depend on it. Every bay reports its engine version and bundle_version with its journal whether it is claimed or not, whether self-update is permitted or not. That is inventory. It is also what makes a stale unclaimed bay findable at all: when a player's rounds keep landing on the same unclaimed bay, SGA may ask that player whether the simulator is theirs and offer them the claim — which is an invitation to consent, not a way around it.
None of this applies to bundles. A bundle is declarative data; it updates on every bay, claimed or not, exactly as the rest of this chapter describes. The distinction in §4 is the whole reason the two can be treated differently.
4b. When a permitted engine update may install: the idle window#
Permission is not timing. An engine with standing permission to update itself still MUST NOT do it while anyone is playing.
An engine MUST NOT begin an update during an active round. No exceptions, no timeout that overrides it, and no "the round looks abandoned" heuristic. A round that is open is a round somebody may come back to.
A bay is idle when either holds:
- no simulator is connected, or
- a simulator is connected but no round-related traffic has arrived for
idle_before_update_ms.
The second case is the one that matters in a commercial facility: the bay host is powered on and the simulator software is running all day, and "connected" says nothing about whether anybody is standing in it.
Any round-related traffic resets the timer. A bay that has just been used is not idle, and a partly elapsed timer carries no credit forward. An update waits for a fresh, whole idle period.
Both durations are carried in the bundle, not compiled into the engine: idle_before_update_ms, and stage_lead_ms for the staging lead below. A threshold that needed a new binary to tune would be a code change to fix a policy mistake, which is exactly the split §4 exists to avoid.
Staging runs ahead of the window, not inside it. Once a bay has been continuously idle for idle_before_update_ms minus stage_lead_ms — a lead of fifteen minutes is the expected default — the engine downloads and stages the package, so that it is on disk and verified at the moment the bay qualifies rather than beginning a download then. An engine that already holds a verified package for that version downloads nothing. If stage_lead_ms is at least the threshold, staging begins as soon as the bay goes idle.
The running engine verifies the signature before it initiates the update. The process that is already trusted does the checking, before it hands control to anything: signature, version ordering, and the operating system's own signature on the package. An updater that verified its own payload would be deciding whether to trust itself.
If a round starts at any point before the swap, the update is abandoned and the timer resets. The staged package stays on disk — it is already verified and already version-ordered — so the next idle window costs nothing to reach.
5. Bundle version in the trust chain#
Every session manifest, every shot record, and every uploaded round carries the bundle_version in force.
That is what makes a result reproducible after the fact. "This round was scored under bundle 2026.09.1, protocol 1.0, ruleset SGA-CR-2.4" is a complete statement of the technical regime that produced it. Three years later, a disputed round can be re-evaluated under exactly the rules that applied to it — not under whatever we happen to believe today.
Our audit requirement, made mechanical.
Publishing drops what has expired#
A bundle build MUST drop any grace extension whose not_after has passed (14 §4.3), and SHOULD report what it dropped rather than doing it silently.
This is the one place bundle publishing edits its own input, and the reason is that the extension list is read by people. An engine ignores an expired extension regardless, so nothing depends on this for correctness — it exists so that "which bays are currently running on an exception" stays a question somebody can answer by looking.
6. The archive: what a bundle version has to still mean in 2031#
Section 5 says a disputed round can be re-evaluated under exactly the rules that applied to it. That is a promise, and until now nothing in this file made it true. These are the requirements that do.
6.1 Permanent retention#
- Every bundle ever published MUST remain retrievable, permanently, by version, at a stable address, with its original signature intact.
- Retrieval MUST NOT require an account. A vendor diagnosing their own three-year-old firmware should not have to ask us for permission to read the vocabulary it was built against.
- A bundle, once published, is immutable. If it is wrong, the correction is a new version. Editing a published bundle would make every record that cites it unverifiable and every earlier diagnosis unreliable.
That is a small commitment in storage — the entire history of the catalog is a few hundred megabytes for the life of the platform — and an enormous one in credibility. It is the difference between "scored under bundle 2026.09.1" being a fact and being a label.
6.2 Prose and catalog move together#
A bundle carries the machine-readable catalog. The specification explains what the fields mean, and the two are useless apart when the question is whether a device behaved correctly.
Each bundle release MUST correspond to a tagged revision of the specification. Given bundle_version, both the catalog and the prose that described it are recoverable, together, as they stood.
6.3 Per-field history#
Every field carries its own record: which bundle introduced it, which amended it and why, whether it is deprecated and what replaced it.
That is what turns "this value looks wrong" into "this value was valid under the definition in force, and the definition changed in 2027.02 for this reason." Those are very different conversations to have with a manufacturer.
6.4 The rule this exists for#
Conformance is evaluated against the bundle in force when the data was produced, never against the current one.
A device built against an old bundle and following it faithfully is not wrong. If a validation range was widened, a plausibility bound retuned, or a vocabulary extended, a device that predates the change was correct at the time and judging it by today's catalog manufactures a false finding — and, worse, one that points at a vendor who did nothing wrong.
This has a concrete consequence for tooling: the conformance kit must be able to load a historical bundle and evaluate against it. The kit today carries its rules internally and always judges by the present, which is right for a vendor checking new work and wrong for anyone diagnosing an old record. Not yet built; noted so it is built before it is needed rather than after.
6.5 The workflow it enables#
a shot record from 2026
| bundle_version: 2026.09.1
v
the archived bundle 2026.09.1 <- signature still verifies
| the field catalog as it stood
v
the tagged specification revision <- what the field MEANT
|
v
judge the device's output against what was required of it THENWhich answers the question worth answering — not "is this data valid today" but "did this implementation do the right thing at the time" — and answers it without anyone having to remember, or having kept notes.
There is a second use that costs nothing extra: reading the diff between two bundles is a precise, dated history of what the industry learned. Every field promoted from a vendor extension into the core is a record of something a manufacturer needed that nobody had anticipated.