P5.x-A: Constants.SignalRMessages catalog (13 names) + SignalRGroupHelper.HubName;
all backend send sites and web subscriptions typed against the shared-manifest
union (SignalRContext.on/off) — unknown names are tsc errors. Dead eventStream
session-payload handling removed from diagnosis page.
P5.x-B: RowKeyCodec as the single inverted-tick encoder (persisted widths pinned,
UserActivity D20 legacy); DeletionRowDump.Table on tombstone rows with
DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic
(legacy fallback retained for pre-field manifests).
P7.1: PATCH config/{tenantId}/fields flipped to TenantAdminOrGA (phase 2);
Settings sections save via per-section diff PATCH from sectionFieldMap.ts —
the ~39-field stale round-trip vector is gone; legacy webhook clearing now only
on the notifications save; agent-settings reset no longer discards contact edits.
P7.2/7.3: useGlobalAdminScope is a projection over useAggregatedAdminScope
(resolveConcreteScopeView, test-pinned); scopedApi builders own the tenant/global
endpoint-pair routing for all 11 former hand-rolled call sites; scopedFetch adds
the shared JSON fetch layer.
P7.4: navVisibility extraction + roles x nav-config matrix and nav<->route-guard
consistency test with a ratchet for the two known pre-existing mismatches
(Operator and GlobalReader vs /settings — user decision pending).
P7.5: web build stamp public/version.json (prebuild, GITHUB_SHA), SWA no-store
route, deploy-web verify poll + versions/web.json manifest, Portal Build card.
P7.6: OkAsyncBaselineGuardTests freezes the 44 anonymous-response sites
(shrink-only ratchet).
Verified: dotnet build 0W/0E, Functions.Tests 4123, DecisionCore 519, V2.Core
2206, SummaryDialog 44; tsc 0, ESLint 0, vitest 762/762; mutation proofs for all
new drift nets. Docs: docs/drift-guards.md + versioning.md web row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.8 KiB
type, title, description, resource, tags, timestamp
| type | title | description | resource | tags | timestamp | ||||
|---|---|---|---|---|---|---|---|---|---|
| Concept | Version Contract — Agent, Backend, MCP, Web | One version shape for all shipping components, the ETag-CAS counter blob that mints build numbers, and the published manifests that make "what is deployed" checkable from outside. | /.github/scripts/Request-BuildNumber.ps1 |
|
2026-07-22T00:00:00+02:00 |
Version Contract
Four components ship from this repository — the agent, the Functions backend, the
MCP server and the web portal. They share one version shape, one way of minting build
numbers, and one manifest format, so that a version string means the same thing
whoever emits it. The web portal is the commit-only exception: a static export has no
runtime that could report a major.minor.build, so it carries commit + buildUtc
alone (no counter blob).
version = <major>.<minor>.<build> major.minor curated by hand, build minted per build
commit = 7-char short SHA of this repo
buildUtc = ISO-8601 build moment
| Component | major.minor from | Counter blob | Reported by |
|---|---|---|---|
| Agent V2 | VersionPrefix in AutopilotMonitor.Agent.V2.csproj |
agent/buildcounter-v2.txt |
agent/version.json |
| Backend | VersionPrefix in AutopilotMonitor.Functions.csproj |
versions/backend.txt |
GET /api/health |
| MCP | version in package.json |
versions/mcp.txt |
GET /health, MCP handshake |
| Web portal | — (commit-only) | — | GET /version.json on the portal host |
Schema
Build numbers — reserve before build, ETag-CAS
.github/scripts/Request-BuildNumber.ps1 is the only implementation; the CI
workflows and the local build.ps1 all call it.
- Anonymous
GETon the counter blob → valueNandETag. The blob holds the last used number as plain text and is public-read; only the write needs the container SAS. PUT N+1withIf-Match: <etag>. The quoted ETag form is required — PowerShell strips the quotes on read, and an unquotedIf-Matchis rejected client-side.412 Precondition Failed→ a concurrent build reserved first → re-read and retry (5 attempts, linear backoff).- Build with the reserved number.
Reserving before the build is what makes concurrent minting impossible. A failed build burns a number — deliberate: uniqueness matters, density does not.
Why a counter and not the published manifest: manifests are written on publish, not on
build. The agent writes version.json only for stable releases, so deriving the next
number from it would hand two consecutive dev builds the same number and let the second
overwrite the first's artifact. A state-free scheme such as git rev-list --count HEAD
fails for the same reason — two builds of one commit collide.
Seeding (once per component): PUT <last used number> with If-None-Match: *, so
an existing counter can never be clobbered. Agent V2 was seeded with 1356 on
2026-07-17; versions/backend.txt with 999 and versions/mcp.txt with 299 on
2026-07-22.
Local builds
No counter is reached, the build number stays 0, and the commit is empty or
unknown. A 1.5.0 with no commit hash on /api/health is therefore a developer
machine, not a release — the fallback is the signal, not a defect. The agent is the
exception: build.ps1 produces uploadable artifacts and reserves a real number unless
-SkipUpload is passed.
Manifests
versions/backend.json and versions/mcp.json (public-read) record what is deployed:
{ "component": "backend", "version": "1.5.1000", "commit": "abc1234",
"buildUtc": "…", "deployedUtc": "…", "runId": "…" }
The MCP manifest adds docsCommit, because for that component "which build" and "which
documentation" are separate questions. versions/web.json carries component, commit,
deployedUtc and runId (no version/buildUtc — the served /version.json holds the
build moment). The portal's own stamp is generated into public/version.json by
scripts/generate-version.js (prebuild) from GITHUB_SHA, served with
Cache-Control: no-store, and deploy-web.yml polls it until the deployed commit
matches the pushed SHA before publishing the manifest.
Each is written last and only after the deploy workflow has polled the component's health endpoint until it reports that exact version and commit. A green deploy step alone means the package was accepted, not that it is being served. Consequently a manifest that disagrees with the live endpoint means the running instance drifted after the fact (rollback, stale revision) rather than a half-finished pipeline.
The agent keeps its own manifest at agent/version.json — the bootstrap derives that
URL from the download URL and verifies the ZIP against its sha256, so it cannot move.
It carries the same commit and buildUtc fields; sha256 and bootstrapVersion
remain, being the customer-facing integrity contract.
Consequences
- Bumping
major.minoris a human decision, made in the component's own file (VersionPrefix, orversioninpackage.json). Nothing bumps it automatically. - A new component joins by seeding a counter blob and calling the same script; no new mechanism.
- The counter blobs and manifests live in the
versionscontainer onautopilotmonitoreu(writes viaAZURE_BLOB_VERSIONS_SAS_TOKEN). The agent's counter stays in theagentcontainer next to the artifacts it numbers.
Citations
.github/scripts/Request-BuildNumber.ps1— the CAS protocol.github/workflows/deploy-backend.yml,deploy-mcp.yml— reserve, verify, publish.github/workflows/deploy-web.yml,src/Web/autopilot-monitor-web/scripts/generate-version.js— web stamp, verify, publish.github/workflows/build-agent.yml,scripts/Deployment/build.ps1— agent line- Build Counter Blob — agent-specific consequences