DEV-000045: attest production artifact revision #15
@@ -4,6 +4,7 @@ services:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/images/postgres.Dockerfile
|
||||
args: { SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-assistant}
|
||||
@@ -31,7 +32,7 @@ services:
|
||||
volumes: ["assistant_nats:/data"]
|
||||
|
||||
notification-receiver:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: notification-receiver } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: notification-receiver, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8085"
|
||||
@@ -50,7 +51,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
core-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: core-runtime } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: core-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8080"
|
||||
@@ -74,7 +75,7 @@ services:
|
||||
- assistant_core_secrets:/run/secrets:ro
|
||||
|
||||
continuity-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8081"
|
||||
@@ -87,7 +88,7 @@ services:
|
||||
COMMIT_EXECUTOR_TOKEN: ${COMMIT_EXECUTOR_TOKEN}
|
||||
|
||||
commit-runtime:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: commit-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8082"
|
||||
@@ -109,12 +110,12 @@ services:
|
||||
- assistant_notification_writer_secret:/run/notification-writer:ro
|
||||
|
||||
cognitive-worker:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: cognitive-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment: { LISTEN_ADDR: ":8083" }
|
||||
|
||||
creator-web:
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web } }
|
||||
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LISTEN_ADDR: ":8084"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
FROM postgres:18.6-alpine
|
||||
ARG SOURCE_REVISION=unknown
|
||||
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
|
||||
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
|
||||
|
||||
COPY database/bootstrap/001_roles_and_schemas.sql /docker-entrypoint-initdb.d/001_roles_and_schemas.sql
|
||||
COPY deploy/images/002_owner_passwords.sh /docker-entrypoint-initdb.d/002_owner_passwords.sh
|
||||
|
||||
@@ -7,6 +7,9 @@ ARG TARGET
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/runtime ./cmd/${TARGET}
|
||||
|
||||
FROM alpine:3.23
|
||||
ARG SOURCE_REVISION=unknown
|
||||
LABEL org.opencontainers.image.revision=$SOURCE_REVISION \
|
||||
org.opencontainers.image.source="https://git.cin.su/m/Assistant.git"
|
||||
RUN addgroup -S assistant && adduser -S -G assistant assistant
|
||||
USER assistant
|
||||
COPY --from=build /out/runtime /usr/local/bin/assistant-runtime
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidatePattern('^[0-9a-f]{40}$')]
|
||||
[string]$Revision,
|
||||
[string]$DockerContext = 'docker-prod',
|
||||
[string]$LocalManifestPath = "$env:LOCALAPPDATA\Domovoy\monitoring\deployment-manifest.json",
|
||||
[string]$NasManifestPath = '\\nas\MST\codex\_monitoring\domovoy\deployment-manifest.json',
|
||||
[string[]]$ExpectedContainers = @(
|
||||
'assistant-postgres-1',
|
||||
'assistant-notification-receiver-1',
|
||||
'assistant-core-runtime-1',
|
||||
'assistant-continuity-runtime-1',
|
||||
'assistant-commit-runtime-1',
|
||||
'assistant-cognitive-worker-1',
|
||||
'assistant-creator-web-1'
|
||||
)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Write-AtomicJson([string]$Path, [string]$Json) {
|
||||
$directory = Split-Path -Parent $Path
|
||||
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
||||
$partial = Join-Path $directory ('.' + [IO.Path]::GetFileName($Path) + ".${PID}.partial")
|
||||
try {
|
||||
[IO.File]::WriteAllText($partial, $Json + "`n", [Text.UTF8Encoding]::new($false))
|
||||
Move-Item -LiteralPath $partial -Destination $Path -Force
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $partial) { Remove-Item -LiteralPath $partial -Force }
|
||||
}
|
||||
}
|
||||
|
||||
$artifacts = foreach ($container in $ExpectedContainers) {
|
||||
$raw = ((& docker.exe --context $DockerContext inspect $container --format '{{json .}}') -join '').Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { throw "Cannot inspect $container" }
|
||||
$inspect = $raw | ConvertFrom-Json
|
||||
$actual = [string]$inspect.Config.Labels.'org.opencontainers.image.revision'
|
||||
if ($actual -ne $Revision) { throw "$container revision mismatch: expected=$Revision actual=$actual" }
|
||||
if ($inspect.State.Status -ne 'running') { throw "$container is not running" }
|
||||
[ordered]@{ container = $container; image_id = [string]$inspect.Image; revision = $actual }
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
version = 1
|
||||
project = 'domovoy'
|
||||
revision = $Revision
|
||||
promoted_at_utc = [DateTime]::UtcNow.ToString('o')
|
||||
artifacts = @($artifacts)
|
||||
}
|
||||
$json = $manifest | ConvertTo-Json -Depth 5
|
||||
Write-AtomicJson $LocalManifestPath $json
|
||||
Write-AtomicJson $NasManifestPath $json
|
||||
Write-Output "Production artifact confirmed: revision=$Revision containers=$($artifacts.Count)"
|
||||
@@ -7,6 +7,7 @@ param(
|
||||
[string]$NasBackupRoot = '\\nas\MST\codex\_backups\domovoy\postgres',
|
||||
[string]$NasStatusPath = '\\nas\MST\codex\_monitoring\domovoy\status.json',
|
||||
[string]$LocalStatusPath = "$env:LOCALAPPDATA\Domovoy\monitoring\status.json",
|
||||
[string]$DeploymentManifestPath = "$env:LOCALAPPDATA\Domovoy\monitoring\deployment-manifest.json",
|
||||
[double]$MaxProductionBackupAgeHours = 30,
|
||||
[double]$MaxNasBackupAgeHours = 36,
|
||||
[int]$MinimumCertificateDays = 21,
|
||||
@@ -60,6 +61,21 @@ foreach ($container in $ExpectedContainers) {
|
||||
catch { Add-Check "container:$container" $false $_.Exception.Message }
|
||||
}
|
||||
|
||||
try {
|
||||
$manifest = Get-Content -LiteralPath $DeploymentManifestPath -Raw | ConvertFrom-Json
|
||||
if ($manifest.version -ne 1 -or $manifest.revision -notmatch '^[0-9a-f]{40}$') { throw 'invalid deployment manifest' }
|
||||
if (@($manifest.artifacts).Count -ne 7) { throw 'deployment manifest artifact count mismatch' }
|
||||
foreach ($artifact in $manifest.artifacts) {
|
||||
$raw = ((& docker.exe --context $DockerContext inspect $artifact.container --format '{{json .}}') -join '').Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) { throw "cannot inspect $($artifact.container)" }
|
||||
$inspect = $raw | ConvertFrom-Json
|
||||
$label = [string]$inspect.Config.Labels.'org.opencontainers.image.revision'
|
||||
if ($label -ne $manifest.revision -or [string]$inspect.Image -ne [string]$artifact.image_id) { throw "artifact drift: $($artifact.container)" }
|
||||
}
|
||||
Add-Check 'deployment-artifact' $true "revision=$($manifest.revision) containers=$(@($manifest.artifacts).Count)"
|
||||
}
|
||||
catch { Add-Check 'deployment-artifact' $false $_.Exception.Message }
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri $Endpoint -TimeoutSec 15
|
||||
$ok = $response.status -eq 'ok'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000044-post-notification-production-audit.json",
|
||||
"task_id": "DEV-000044",
|
||||
"task_revision": 3
|
||||
"task_file": "dev/tasks/DEV-000045-production-artifact-provenance.json",
|
||||
"task_id": "DEV-000045",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000045",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0005",
|
||||
"base_repo_revision": "959c89d47bf231dbcfb7a05e7b63b0e8579e3f0c",
|
||||
"risk_tier": "E",
|
||||
"intent": "Stamp project-built production images with the immutable Git revision, record promoted image digests in an external deployment manifest, and make production health reject provenance drift without using docker-test.",
|
||||
"requirement_ids": ["INV-TOPO-001", "DEV-000044-FINDING-ARTIFACT-PROVENANCE", "USER-DOCKER-PROD-ONLY"],
|
||||
"allowed_change_scope": {"modules": [], "paths": ["deploy/docker-compose.prod.yml", "deploy/images/runtime.Dockerfile", "deploy/images/postgres.Dockerfile", "deploy/windows/Confirm-DomovoyProductionArtifact.ps1", "deploy/windows/Test-DomovoyProductionHealth.ps1", "docs/DEPLOYMENT.md", "docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000045-production-artifact-provenance.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "security/**", "tests/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0},
|
||||
"contracts": {"consume": ["existing production Compose topology", "Docker OCI image metadata"], "may_change": []},
|
||||
"semantic_owner": "none-operational-verification",
|
||||
"canonical_state": [],
|
||||
"state_transitions": [],
|
||||
"side_effects_and_privileges": ["rebuild and replace project-owned production containers with exact merged source", "write non-secret deployment manifest to local profile and private NAS monitoring directory"],
|
||||
"persistence_and_migrations": ["non-secret external deployment manifest only"],
|
||||
"failure_retry_restart": ["manifest is published atomically only after all expected labels and digests verify", "failed verification leaves previous manifest intact", "health becomes degraded on revision or digest drift"],
|
||||
"assumptions": ["docker context docker-prod is the production endpoint", "Git HEAD is merged origin/main before promotion", "docker-test remains prohibited"],
|
||||
"acceptance_scenarios": ["every project-built image has the merged Git revision OCI label", "manifest records revision and exact image digests", "health verifies running labels and digests against the manifest", "production remains healthy after exact-artifact promotion"],
|
||||
"required_tests": ["PowerShell parser", "task schema", "architecture preflight", "exact diff scope", "go test", "go vet", "Compose config", "production artifact confirmation", "production health", "CI", "docker-test absence"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -14,6 +14,15 @@ Temporal is intentionally **not** the canonical owner of future obligations. If/
|
||||
|
||||
The initial stack definition is `deploy/docker-compose.prod.yml`. Portainer may deploy it from Git after the repository has been pushed.
|
||||
|
||||
Project-built production images receive `org.opencontainers.image.revision` and `org.opencontainers.image.source` OCI labels. Promotion must set `ASSISTANT_SOURCE_REVISION` to the full merged `origin/main` SHA before `docker compose build/up`; the default `unknown` is intentionally rejected by artifact confirmation. After all project containers are running, execute:
|
||||
|
||||
```powershell
|
||||
$revision = git rev-parse origin/main
|
||||
powershell -ExecutionPolicy Bypass -File .\deploy\windows\Confirm-DomovoyProductionArtifact.ps1 -Revision $revision -DockerContext docker-prod
|
||||
```
|
||||
|
||||
The confirmation script accepts the promotion only when every project-built container has that revision label, then atomically records its exact Docker image ID in `%LOCALAPPDATA%\Domovoy\monitoring\deployment-manifest.json` and the private NAS monitoring directory. Production health compares every running label and image ID to this manifest and becomes degraded on drift. The manifest is non-secret operational evidence; it is not canonical Assistant state.
|
||||
|
||||
The first external operation adds `assistant-notification-receiver-1` on production host port `18081`. Caddy applies `deploy/caddy/assistent.caddy`, routing only `https://assistent.cin.su/delivery/*` to that port and all other paths to Creator Web on `18080`. The receiver has an isolated durable volume and no Assistant database access. Commit success means an immutable receiver receipt exists; Windows display and human reading are not Commit outcomes.
|
||||
|
||||
Migrations `010_capability_notification_binding.sql` and `011_commit_notification_envelope.sql` must be applied before the new runtimes start. The initial Capability binding is owner-local for `domovoy-creator-principal`, revision 1, capability `prospective-notification-delivery`, origin `https://assistent.cin.su`, delivery path `/delivery/v1/deliveries`, receipt path `/delivery/v1/receipts`, protocol `IDEMPOTENT_RECEIPT_V1`, and secret reference `domovoy-notification-write-v1`.
|
||||
|
||||
@@ -20,9 +20,10 @@ Audit date: 2026-08-16. Active audit: DEV-000044 revision 3.
|
||||
|
||||
## Remaining work requiring a new task decision
|
||||
|
||||
1. **Artifact provenance:** running images expose Compose image digests but no OCI Git revision label, and Go build metadata reports the module as `(devel)`. Operational behavior is verified, but the source revision of an already-running image cannot be independently proven from that image alone. A deployment task should stamp the immutable Git revision and record the promoted image digests.
|
||||
2. **Cognitive provider:** the cognitive worker intentionally remains the deterministic fake. Selecting a real model/provider, data policy and allowed proposal contract requires a separate capability/architecture decision; it must not gain canonical mutation authority.
|
||||
3. **Product breadth:** notification delivery is the only accepted real external operation. Any additional operation needs its own proposal, authority model, idempotency/reconciliation semantics and Capability binding.
|
||||
4. **Artifact and Epistemics product behavior:** their ownership boundaries exist, but no product task has yet specified durable user-facing workflows for them.
|
||||
1. **Cognitive provider:** the cognitive worker intentionally remains the deterministic fake. Selecting a real model/provider, data policy and allowed proposal contract requires a separate capability/architecture decision; it must not gain canonical mutation authority.
|
||||
2. **Product breadth:** notification delivery is the only accepted real external operation. Any additional operation needs its own proposal, authority model, idempotency/reconciliation semantics and Capability binding.
|
||||
3. **Artifact and Epistemics product behavior:** their ownership boundaries exist, but no product task has yet specified durable user-facing workflows for them.
|
||||
|
||||
DEV-000045 closes the DEV-000044 artifact-provenance finding for new promotions by stamping the merged Git revision into OCI labels, recording exact promoted image IDs outside the repository, and making production monitoring reject label or digest drift. Images that predate DEV-000045 remain historically unverifiable from the image alone.
|
||||
|
||||
These are explicit scope decisions, not hidden incomplete steps in DEV-000043. No production secret is stored in the repository.
|
||||
|
||||
Reference in New Issue
Block a user