Choreography 5 — Versioning & compatibility
What the Protocol version promises you, what your client should pin, and what it must tolerate. This is RFC §5.3 made adopter-facing: the rules below are the same conventions Harbor's own tooling lives by, promoted to the contract third-party clients can build against.
Methods demonstrated:
runtime.info
The promise
The Harbor Protocol is versioned independently of the Runtime implementation, as a semver triple pinned in one place in the Runtime's sources (internal/protocol/types/version.go, currently 0.1.0). The discipline:
- Bumping the Protocol version is an RFC change — never a side effect of a Runtime refactor. A Runtime release that adds methods, events, fields, or capabilities without breaking anything does NOT bump it.
- Same major ⇒ compatible. A client built against
0.1.0can talk to a Runtime speaking any0.x.y. Compare the major component mechanically; don't string-compare the triple. - Breaking changes get a deprecation window. A breaking change ships as a structured deprecation notice (the element being retired, the version it is deprecated in, the version it will be removed in, and the migration) before removal — third-party clients are never whipsawed. At
0.1.0the deprecation registry is empty: nothing has ever been deprecated.
Where the version surfaces on the wire
Twice, so you can check it cheaply at attach AND continuously:
1. The attach handshake — runtime.info (POST /v1/control/runtime.info). A real response from a dev Runtime:
{
"instance_id": "harbor-dev-192.168.1.7",
"display_name": "harbor dev",
"build_version": "v0.0.0-dev",
"build_commit": "dev",
"build_go_version": "go1.26.3",
"protocol_version": "0.1.0",
"capabilities": [
"events_subscribe",
"runtime_posture",
"task_control"
],
"uptime_seconds": 16
}Note the two version fields: build_version is the Runtime's release version (its own semver, moving independently); protocol_version is the wire contract. Pin against the latter.
2. Every control-surface response carries protocol_version — visible in every example across this track ({"accepted": true, "method": "cancel", "protocol_version": "0.1.0"}). A long-lived client can detect a Runtime upgrade mid-session without re-handshaking.
What to pin
Two things, both read at attach:
- The Protocol major you were built against. Refuse (or warn loudly) on a major mismatch — that is the one signal that wire shapes you depend on may have changed underneath you.
- The capability set you require.
runtime.info.capabilitiesis the Runtime advertising which Protocol surfaces are live — capability strings liketask_control,events_subscribe,runtime_posture,topology_snapshot(the response shape isRuntimeInfo). Read it and shape your client: the dev Runtime above advertises three capabilities and nottopology_snapshot— a dashboard that wants the topology projection degrades that panel instead of crashing. This is how Harbor's own Console composes against stripped-down runtimes; third-party clients get the same gate.
The worked event-viewer client does exactly this — seven lines: parse protocol_version, compare majors, scan capabilities for events_subscribe.
What to tolerate
The flip side of the pin. Two tolerance rules keep your client working across every non-breaking Runtime upgrade:
Unknown fields: ignore them
New response fields and new event payload keys are additive. A 0.x Runtime may start sending fields your client has never seen — in runtime.info, in a task snapshot, in an event payload. Never fail on an unrecognized field; decode what you know and ignore the rest (the default behavior of virtually every JSON decoder — don't switch yours to a reject-unknown mode against this wire). The same applies to enum-shaped string sets your client branches on: a new event type, a new capability string, or a new task status in a future minor version must fall into your default: branch, not crash it.
(The Runtime is stricter about what it accepts than what you must accept — an unknown method-specific request shape is rejected invalid_request. The tolerance rule is about what arrives at YOUR decoder.)
Unknown methods: 404/405 means "not served here", so degrade
A method name outside the Runtime's canonical registry returns the canonical error envelope, captured live:
POST /v1/control/snapshot_universe
→ HTTP 404
{"code":"unknown_method","message":"method \"snapshot_universe\" is not a canonical Protocol method"}A wrong HTTP verb on a known route returns a bare 405 Method Not Allowed (no JSON envelope — it never reaches the Protocol layer). Treat both the same way: this runtime does not serve that surface — degrade the feature, don't crash. A 404 with code: "unknown_method" (or 501 from a driver that cannot satisfy a capability, e.g. presign_unsupported) is a composition signal, not an error condition.
This convention is load-bearing inside Harbor itself: every phase smoke script in the repo encodes it as the SKIP discipline — scripts/smoke/common.sh::skip_if_404 treats 404/405/501 as "surface not built yet, skip the assertions" so that newer test suites coexist with older builds. Your client should encode the same posture: probe (or read capabilities), then enable.
Don't couple to non-contract details
Two things this track deliberately does NOT promise, so don't pin them: intra-step event ordering beyond the per-bus sequence numbers (see the ordering note in the pause model), and message strings on error envelopes — branch on code, never on prose (errors).
The attach sequence, assembled
POST /v1/control/runtime.info
→ protocol_version: compare major against what you were built for
→ capabilities: enable/disable your features
open GET /v1/events; drive the run lifecycle per task-control
on 404 unknown_method / 405 / 501 at any later point: degrade that feature
on a protocol_version major change mid-session (runtime upgraded under
you): re-handshake, re-snapshot, resubscribeWhen the contract does change
The first breaking Protocol change will: bump the major, land as an RFC change, ship structured deprecation notices for a published window before removal, and version this documentation track alongside it (these pages describe 0.x). Additive evolution — new methods, events, fields, capabilities — keeps arriving in minors, covered by the tolerance rules above, and regenerates the reference pages in the same commit, mechanically (make protocol-docs-gen-check gates a wire change whose docs didn't move).