A manual exact-SHA deployment currently requires the operator to provide the same 40-character commit twice:
NEO_REF=<sha>
NEO_REVISION=<sha>
That duplication is not a second decision. It is an implementation detail left exposed by the provenance work in #15774 and the resolve-once reference pipeline in #15792.
The three provenance facts remain valid and distinct:
the selector requested before resolution;
the commit packaged into the image;
the commit measured inside the built artifact.
The remaining friction is at the operator boundary: once a selector has resolved to one commit, a human should enter that commit once.
The Problem
For a pinned deployment, NEO_REF and NEO_REVISION must contain the same resolved SHA. Requiring both values adds no information, but it creates a mismatch state where the source fetch can use one commit while the OCI revision label asserts another.
The current reference pipeline already demonstrates the intended shape: it accepts one NEO_SELECTOR, resolves it once, then exports both Docker-facing values from the same resolved_revision. Manual Compose use does not have that adapter and exposes both internal build arguments directly.
The Dockerfile also lacks a hard equality check between a non-empty asserted NEO_REVISION and the independently generated /app/.neo-revision. A caller can therefore stamp an image with a revision claim that disagrees with its packaged source.
The Architectural Reality
ai/examples/cloud-deployment/deploy-pipeline.sh owns the pre-resolution selector. It resolves branch, tag, or SHA input once and currently exports both NEO_REF and NEO_REVISION from the resulting full commit.
ai/deploy/docker-compose.yml forwards both variables independently into the kb-server, mc-server, and orchestrator builds.
ai/deploy/Dockerfile still needs separate internal build arguments: NEO_REF drives source acquisition and labels the requested ref; NEO_REVISION drives the OCI packaged-revision label. Docker labels cannot derive a later-stage build argument from a file emitted by an earlier stage.
/app/.neo-revision remains the independently measured artifact truth and must not be replaced by caller input.
The structure map confirms that ai/deploy and ai/examples/cloud-deployment already own this surface; no new module or placement decision is needed.
The Fix
Define one operator-facing resolved deployment pin at the Compose/manual-deploy boundary. Reuse NEO_REVISION rather than introducing a third environment variable.
Map that one pin internally to both Docker build arguments for all three services:
source fetch input (NEO_REF);
OCI packaged-revision assertion (NEO_REVISION).
Keep NEO_SELECTOR as the pre-resolution pipeline input. Branches and tags resolve before reaching Compose; they do not become OCI revision values.
Preserve the current unpinned path: with the resolved pin unset, source acquisition defaults to dev and the OCI revision remains empty.
Add a Docker build-time integrity assertion: when NEO_REVISION is non-empty, it must equal the SHA written to /app/.neo-revision; fail the build with a clear error when they differ.
Update the deployment guide and focused tests so no documented manual path asks a human to duplicate the SHA.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Evidence
Manual / Compose resolved pin
ai/deploy/docker-compose.yml
one full SHA supplied once and mapped to both internal build arguments for all three services
unset keeps current unpinned behavior
Compose render tests
Pipeline selector
ai/examples/cloud-deployment/deploy-pipeline.sh
branch, tag, or SHA resolves once; one resolved commit reaches Compose
existing selector default remains unchanged
DeployPipelineRevisionPin.spec.mjs
Docker source-fetch argument
ai/deploy/Dockerfile
receives the canonical resolved pin as internal NEO_REF
dev when no pin is supplied
focused Docker/Compose contract test
OCI packaged revision
ai/deploy/Dockerfile
receives the same canonical pin as internal NEO_REVISION; never a mutable selector
empty means not asserted
image provenance parity test
/app/.neo-revision
source stage in ai/deploy/Dockerfile
remains independently generated actual revision; mismatch with a non-empty assertion fails the build
local-build for local source mode
mutation test plus parity receipt
Operator documentation
learn/agentos/cloud-deployment/PipelineWiring.md
shows one-pin manual invocation and resolve-once pipeline flow
none
documentation assertion
Decision Record Impact
Aligned with existing decisions; no ADR change. This tightens the implementation boundary established by #15774 and #15792. It does not collapse requested, resolved, and observed provenance into one fact, and it does not choose rollout authority.
Acceptance Criteria
A manual exact-SHA Compose render requires one operator environment value, not duplicate NEO_REF and NEO_REVISION assignments.
That one full SHA reaches both internal Docker build arguments for kb-server, mc-server, and orchestrator.
With the resolved pin unset, all three builds retain the current behavior: source ref dev, OCI revision empty.
The reference pipeline still accepts branch, tag, or SHA selectors, resolves once, and hands one canonical full SHA to the Compose boundary.
A non-empty asserted revision that differs from /app/.neo-revision fails the Docker build with a clear integrity error.
For a correct git-source pin, org.neomjs.image.requested-ref, org.opencontainers.image.revision, and /app/.neo-revision all report the same full SHA across the three-service cohort.
Local-source builds remain honest: /app/.neo-revision is local-build and the OCI revision is empty.
DeployPipelineRevisionPin.spec.mjs and the provenance parity coverage are updated with positive, unset, and mismatch cases.
PipelineWiring.md no longer instructs operators to export the same SHA into two variables.
Out of Scope
Removing or merging the three provenance surfaces.
Changing /app/.neo-revision from measured artifact truth into caller-supplied metadata.
Choosing automated rollout authority, rollout cadence, or rollback policy.
Changes in downstream or private deployment repositories.
Changing local-source provenance semantics.
Avoided Traps
Collapsing selector and resolved revision. The pipeline selector remains distinct and resolves before the build boundary.
Putting mutable dev into the OCI revision label. Unpinned builds continue to leave that label empty.
Adding another environment variable. The goal is one operator pin, not a third alias.
Trusting the label assertion without checking the artifact./app/.neo-revision remains the falsifier.
Removing internal Docker arguments because the operator sees one value. Their build-stage roles remain separate even when their pinned value shares one source.
Live latest-open sweep: checked the latest 20 open issues at 2026-07-28T13:40:45Z; no equivalent found. A2A in-flight claim sweep: checked 30 messages across all read states at the same timestamp; no overlapping lane claim or intent found. Semantic Knowledge Base and exact issue/discussion corpus searches found the existing two-variable provenance design, but no consolidation ticket.
Context
A manual exact-SHA deployment currently requires the operator to provide the same 40-character commit twice:
That duplication is not a second decision. It is an implementation detail left exposed by the provenance work in
#15774and the resolve-once reference pipeline in#15792.The three provenance facts remain valid and distinct:
The remaining friction is at the operator boundary: once a selector has resolved to one commit, a human should enter that commit once.
The Problem
For a pinned deployment,
NEO_REFandNEO_REVISIONmust contain the same resolved SHA. Requiring both values adds no information, but it creates a mismatch state where the source fetch can use one commit while the OCI revision label asserts another.The current reference pipeline already demonstrates the intended shape: it accepts one
NEO_SELECTOR, resolves it once, then exports both Docker-facing values from the sameresolved_revision. Manual Compose use does not have that adapter and exposes both internal build arguments directly.The Dockerfile also lacks a hard equality check between a non-empty asserted
NEO_REVISIONand the independently generated/app/.neo-revision. A caller can therefore stamp an image with a revision claim that disagrees with its packaged source.The Architectural Reality
ai/examples/cloud-deployment/deploy-pipeline.showns the pre-resolution selector. It resolves branch, tag, or SHA input once and currently exports bothNEO_REFandNEO_REVISIONfrom the resulting full commit.ai/deploy/docker-compose.ymlforwards both variables independently into thekb-server,mc-server, andorchestratorbuilds.ai/deploy/Dockerfilestill needs separate internal build arguments:NEO_REFdrives source acquisition and labels the requested ref;NEO_REVISIONdrives the OCI packaged-revision label. Docker labels cannot derive a later-stage build argument from a file emitted by an earlier stage./app/.neo-revisionremains the independently measured artifact truth and must not be replaced by caller input.ai/deployandai/examples/cloud-deploymentalready own this surface; no new module or placement decision is needed.The Fix
NEO_REVISIONrather than introducing a third environment variable.NEO_REF);NEO_REVISION).NEO_SELECTORas the pre-resolution pipeline input. Branches and tags resolve before reaching Compose; they do not become OCI revision values.devand the OCI revision remains empty.NEO_REVISIONis non-empty, it must equal the SHA written to/app/.neo-revision; fail the build with a clear error when they differ.Contract Ledger Matrix
ai/deploy/docker-compose.ymlai/examples/cloud-deployment/deploy-pipeline.shDeployPipelineRevisionPin.spec.mjsai/deploy/DockerfileNEO_REFdevwhen no pin is suppliedai/deploy/DockerfileNEO_REVISION; never a mutable selector/app/.neo-revisionai/deploy/Dockerfilelocal-buildfor local source modelearn/agentos/cloud-deployment/PipelineWiring.mdDecision Record Impact
Aligned with existing decisions; no ADR change. This tightens the implementation boundary established by
#15774and#15792. It does not collapse requested, resolved, and observed provenance into one fact, and it does not choose rollout authority.Acceptance Criteria
NEO_REFandNEO_REVISIONassignments.kb-server,mc-server, andorchestrator.dev, OCI revision empty./app/.neo-revisionfails the Docker build with a clear integrity error.org.neomjs.image.requested-ref,org.opencontainers.image.revision, and/app/.neo-revisionall report the same full SHA across the three-service cohort./app/.neo-revisionislocal-buildand the OCI revision is empty.DeployPipelineRevisionPin.spec.mjsand the provenance parity coverage are updated with positive, unset, and mismatch cases.PipelineWiring.mdno longer instructs operators to export the same SHA into two variables.Out of Scope
/app/.neo-revisionfrom measured artifact truth into caller-supplied metadata.Avoided Traps
devinto the OCI revision label. Unpinned builds continue to leave that label empty./app/.neo-revisionremains the falsifier.Related
Related: #15774 Related: #15792 Related: #15787
Live latest-open sweep: checked the latest 20 open issues at 2026-07-28T13:40:45Z; no equivalent found. A2A in-flight claim sweep: checked 30 messages across all read states at the same timestamp; no overlapping lane claim or intent found. Semantic Knowledge Base and exact issue/discussion corpus searches found the existing two-variable provenance design, but no consolidation ticket.
Origin Session ID: 019fa530-53d6-7271-bf05-51497720b29c
Retrieval Hint:
query_raw_memories("NEO_REF NEO_REVISION duplicate operator pin deployment provenance"); inspectai/deploy/docker-compose.yml,ai/deploy/Dockerfile,ai/examples/cloud-deployment/deploy-pipeline.sh, andlearn/agentos/cloud-deployment/PipelineWiring.md.