Complete renamed image source and verify bounded consumer migration readiness
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Failing after 1m2s

Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 23:25:19 +02:00
parent dd15d8cf3f
commit 16ca97b277
13 changed files with 455 additions and 19 deletions

View file

@ -22,7 +22,14 @@ on:
- "examples/**"
- "go.mod"
- "go.sum"
- "tools/publish-runtime-images.sh"
workflow_dispatch:
inputs:
publish_runtime_rename:
description: "Publish both packages after the exact new public-package scope is accepted"
type: boolean
required: false
default: false
env:
REGISTRY: forgejo.coulomb.social
@ -39,7 +46,8 @@ jobs:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
REF="${GITHUB_SHA:-main}"
REF="${GITHUB_SHA:?full source revision required}"
[[ "$REF" =~ ^[a-f0-9]{40}$ ]] || exit 2
SHORT="${REF:0:7}"
mkdir -p buildctx "${HOME}/bin"
wget -qO /tmp/repo.tar.gz \
@ -49,11 +57,13 @@ jobs:
| tar xz --strip-components=1 -C "${HOME}/bin" docker/docker
export PATH="${HOME}/bin:${PATH}"
echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
IMAGE="${REGISTRY}/${IMAGE_NAME}"
docker build -f buildctx/Containerfile -t "${IMAGE}:latest" -t "${IMAGE}:main-${SHORT}" buildctx
docker push "${IMAGE}:latest"
docker push "${IMAGE}:main-${SHORT}"
echo "pushed ${IMAGE}:latest and ${IMAGE}:main-${SHORT}"
# Pushes keep the already admitted legacy package. Paired publication
# is an explicit owner dispatch after the daily red-batch disposition.
MODE=legacy
if [ "${{ github.event_name }}" = workflow_dispatch ] && [ "${{ inputs.publish_runtime_rename }}" = true ]; then
MODE=paired
fi
bash buildctx/tools/publish-runtime-images.sh "$REF" "$MODE" buildctx
- name: Report immutable digest
run: |
@ -61,5 +71,6 @@ jobs:
export PATH="${HOME}/bin:${PATH}"
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SHORT="${GITHUB_SHA:0:7}"
# Deployments pin by digest, never by tag -- print it for the rollout step.
# Legacy package is available on ordinary push; paired digest verification
# runs in the publisher before an owner can switch any pin.
docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE}:main-${SHORT}"

View file

@ -9,7 +9,9 @@ FROM gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c
COPY --from=build /out/access-engine /usr/local/bin/access-engine
# Retired name: prints the rename and exits 2 (FLEX-DEC-2026-020).
COPY --from=build /out/flex-auth /usr/local/bin/flex-auth
COPY examples /opt/access-engine/examples
# Existing pinned releases still pass these paths during the side-by-side migration.
COPY examples /opt/flex-auth/examples
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/access-engine"]
CMD ["serve", "--addr", "0.0.0.0:8080", "--registry", "/opt/flex-auth/examples/tenant-engine/registry_snapshot.json", "--policy", "/opt/flex-auth/examples/tenant-engine/policy_package.md"]
CMD ["serve", "--addr", "0.0.0.0:8080", "--registry", "/opt/access-engine/examples/tenant-engine/registry_snapshot.json", "--policy", "/opt/access-engine/examples/tenant-engine/policy_package.md"]

View file

@ -4,11 +4,14 @@ WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/flex-auth ./cmd/flex-auth
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/access-engine ./cmd/access-engine && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/flex-auth ./cmd/flex-auth
FROM gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab
COPY --from=build /out/access-engine /usr/local/bin/access-engine
COPY --from=build /out/flex-auth /usr/local/bin/flex-auth
COPY examples /opt/access-engine/examples
# Existing pinned releases still pass these paths during the side-by-side migration.
COPY examples /opt/flex-auth/examples
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/flex-auth"]
CMD ["serve", "--addr", "0.0.0.0:8080", "--registry", "/opt/flex-auth/examples/tenant-engine/registry_snapshot.json", "--policy", "/opt/flex-auth/examples/tenant-engine/policy_package.md"]
ENTRYPOINT ["/usr/local/bin/access-engine"]
CMD ["serve", "--addr", "0.0.0.0:8080", "--registry", "/opt/access-engine/examples/tenant-engine/registry_snapshot.json", "--policy", "/opt/access-engine/examples/tenant-engine/policy_package.md"]

View file

@ -2145,3 +2145,32 @@ alias would keep the old name in use, and the rename exists to end that.
`/opt/flex-auth` image paths, the caller audience, Kubernetes names and the
image package. They stay with `FLEX-WP-0036` T03 to T05 and their own order
(`FLEX-DEC-2026-015`).
---
## FLEX-DEC-2026-021 — Runtime target names and bounded side-by-side migration
**Date:** 2026-10-11
**Status:** proposed; exact scope in the single daily red batch
**Workplan:** `FLEX-WP-0036-T01`
**Supersedes on acceptance:** runtime rows of `FLEX-DEC-2026-013`.
The proposed namespace and chart are `access-engine`; pin release, Service,
Deployment and reviewer ServiceAccount names are `access-engine-<pin>`, preserving
the nine existing pin suffixes. DNS is
`access-engine-<pin>.access-engine.svc.cluster.local:8080`. The caller audience is
`access-engine`, with exact legacy `flex-auth` acceptance during migration. The
engine's own resource-system coordinate is `access-engine`; protected consumers
retain their existing identities and grants. No PDP-self resource lane is created.
The binary ruling remains the accepted FLEX-DEC-2026-020: `access-engine` is
the real binary and `flex-auth` is an exit-2 rename stub. Canonical image paths
are `/opt/access-engine/examples`; legacy paths remain during consumer migration.
The image is published as `coulomb/access-engine` and as `coulomb/flex-auth` from
one build, independently verifying equal registry manifest digests. New public
package publication and the nine new TokenReview subjects remain unexecuted
until the exact batch disposition; this source record does not approve them.
Switch consumers individually only after predecessor proofs and side-by-side
allow/deny checks. Remove old egress/grants only after bounded old-twin traffic
and soak evidence; rollback retains the original pins and consumer access.

View file

@ -0,0 +1,42 @@
{
"schema": "access-runtime-source-validation/v1",
"task": "FLEX-WP-0036-T04",
"source_sha256": {
"Containerfile": "85094ed869bbcd24a8b1fadff46038c891fef2d2b2f20b2689a59004b94d19a9",
"Containerfile.lifecycle": "248a36de72c49513dac478367863610a54a1499c22f5505a1cc08a0565eb050d",
".forgejo/workflows/image.yaml": "af7768f4c223b9151060bf1c61d2bc22a09dfe5ad52349409545135660108e27",
"tools/publish-runtime-images.sh": "796f2d402a5d118879f45f47e762004602c612ccf783fc6c60e51ee7f16bca5a",
"internal/adapters/topaz/integration_test.go": "4b506ff3d27f8ad36f4a7d3d2a263e959be32d14857321cfcfd54c9899c702e6"
},
"local_image_tag": "custodian-access-runtime-t04:20261011",
"local_build_passed": true,
"policy_path_checks": [
{
"path": "/opt/access-engine/examples/tenant-engine/policy_package.md",
"exit": 0,
"valid": true,
"checks": 75
},
{
"path": "/opt/flex-auth/examples/tenant-engine/policy_package.md",
"exit": 0,
"valid": true,
"checks": 75
}
],
"legacy_stub_exit": 2,
"legacy_stub_rename_message": true,
"publisher_failure_and_scope_tests": 5,
"go_suite_passed": true,
"registry_publication_proven": false,
"native_rollout": false,
"preparation_failures": [
{
"stage": "local validation invocation",
"exit": 64,
"reason": "incorrect registry/policy flags for validate command",
"recovery": "used documented -kind policy -file; both paths passed"
}
],
"oci_index_handling": "Registry verification uses independent pull and exact repository RepoDigest; retained build provenance/index. No Buildx plugin dependency in publisher."
}

View file

@ -106,3 +106,32 @@ Live pins: `flex-auth-informed-decision-lifecycle`, `-sitting`, `-t03`.
Requested after T01 fixes the target names, so each owner gets one message
with the exact new URL and audience: `ops-warden`, `secrets-engine`,
`user-engine` with `net-kingdom`, `tenant-engine`, and `informed-decision`.
## Deterministic consumer readiness check (2026-10-11, T06)
Run `python3 tools/verify_runtime_consumer_migration.py --estate-root /home/worsch`.
The class parses only the bounded public inventory source paths as YAML/JSON,
TOML or Python literal strings, excludes comments and records file SHA256s.
It emits exact old/new pin origins, source-owner handoff responsibilities and
source migration phases. It never marks native readiness or closes a task.
Unsigned completion flags, source URL strings and a missing log are not live
proof. Operator configuration, ops-bridge ClusterIP tunnels and Secrets CLI
runtime selection remain owner evidence requirements; no private operator
configuration or credentials are read by this tool.
The 2026-10-11 re-derivation found remaining owner source work for ops-warden,
secrets-engine, user-engine with net-kingdom, tenant-engine and informed-decision.
No new endpoint appeared in these bounded files. The five owner handoffs are
prepared by the report, not sent or claimed acknowledged. Source owners first
retain old egress while adding the new namespace, then change one consumer's
endpoint and projected audience after T02/T05 and ordered live prerequisites.
Each new pin needs authenticated allow/deny and exact deployed revision/digest
correspondence; old-twin logs need an explicit observation interval and
no-traffic/soak evidence before old egress removal or T07 retirement.
The earlier `flex-auth-policy-check` catalogue example is a policy routing lane,
not a secret-fetch package lane. Its naming migration does not authorize adding
PDP-self resource systems to packages that govern other protected consumers.
T05's nine new TokenReview ServiceAccount grants are a separate red daily-batch
item. This report does not authorize those grants or the consumer changes.

View file

@ -8,10 +8,12 @@ import (
)
func TestTopazDockerComposeExample(t *testing.T) {
if os.Getenv("FLEX_AUTH_RUN_TOPAZ_INTEGRATION") == "1" {
t.Skip("FLEX_AUTH_RUN_TOPAZ_INTEGRATION was renamed: set ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION=1 (FLEX-WP-0036)")
enabled := os.Getenv("ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION")
if enabled == "" && os.Getenv("FLEX_AUTH_RUN_TOPAZ_INTEGRATION") == "1" {
t.Log("FLEX_AUTH_RUN_TOPAZ_INTEGRATION is deprecated; use ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION=1 (FLEX-WP-0036)")
enabled = "1"
}
if os.Getenv("ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION") != "1" {
if enabled != "1" {
t.Skip("set ACCESS_ENGINE_RUN_TOPAZ_INTEGRATION=1 to run the examples/topaz docker-compose integration")
}

View file

@ -8,6 +8,7 @@ python3 tests/validate_app_toml.py
python3 tests/test_outbox_chart.py
python3 tools/gen_runtime_twins.py --check
python3 tests/test_runtime_twins.py
python3 tests/test_runtime_consumer_migration.py
for values in values/stage1.yaml values/stage2-canary.yaml values/stage3-production.yaml \
values/user-engine.yaml values/tenant-engine.yaml values/ops-warden.yaml; do

View file

@ -0,0 +1,69 @@
#!/usr/bin/env python3
import importlib.util
from pathlib import Path
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("migration", ROOT / "tools/verify_runtime_consumer_migration.py")
migration = importlib.util.module_from_spec(spec); sys.modules[spec.name] = migration; spec.loader.exec_module(migration)
class MigrationTests(unittest.TestCase):
def classify(self, text, network=True):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp); (root / "owner").mkdir(); (root / "owner/runtime.yaml").write_text(text)
consumer = migration.Consumer("owner", ("owner",), ("example",), ("owner/runtime.yaml",), network)
return migration.MigrationReadiness(root).classify(consumer)
def test_ordering_and_no_native_admission(self):
old = "url: http://flex-auth-example.flex-auth.svc.cluster.local:8080\naudience: flex-auth\nkind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: flex-auth}}}]}]}\n"
new = "new: http://access-engine-example.access-engine.svc.cluster.local:8080\naudience: access-engine\nkind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: access-engine}}}]}]}\n"
self.assertEqual(self.classify(old)["phase"], "NEEDS_OWNER_SOURCE_WORK")
self.assertEqual(self.classify(old+"---\n"+new)["phase"], "SOURCE_DUAL_PREPARATION_REVIEW_REQUIRED")
switched = self.classify(new + "native_ready: true\nT05_verified: true\n")
self.assertEqual(switched["phase"], "SOURCE_SWITCHED_NATIVE_PROOF_REQUIRED")
self.assertFalse(switched["native_ready"])
bad_order = self.classify(old.replace("kind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: flex-auth}}}]}]}\n", "") + "---\n" + new)
self.assertEqual(bad_order["phase"], "NEEDS_OWNER_SOURCE_WORK")
self.assertTrue(any("unsafe migration order" in r for r in bad_order["reasons"]))
def test_exact_hosts_and_ports(self):
for origin in ("http://access-engine-example.access-engine.svc.cluster.local.evil:8080", "http://access-engine-example.access-engine.svc.cluster.local:9000", "http://access-engine-example.access-engine.svc.cluster.local:bad", "http://[malformed-template", "http://flex-auth-example.flex-auth.svc.cluster.local:8080"):
with self.subTest(origin=origin):
got = self.classify("url: " + origin+"\naudience: access-engine\nkind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: access-engine}}}]}]}\n")
self.assertEqual(got["new_endpoint_pins"], [])
self.assertEqual(got["phase"], "NEEDS_OWNER_SOURCE_WORK")
def test_unrelated_namespace_label_is_not_egress(self):
got = self.classify("url: http://access-engine-example.access-engine.svc.cluster.local:8080\naudience: access-engine\nmetadata: {labels: {kubernetes.io/metadata.name: access-engine}}\n")
self.assertFalse(got["egress_namespace"]["new"])
self.assertEqual(got["phase"], "NEEDS_OWNER_SOURCE_WORK")
def test_comments_cannot_supply_migration_facts(self):
got = self.classify("# url: http://access-engine-example.access-engine.svc.cluster.local:8080\n# audience: access-engine\n# kind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: access-engine}}}]}]}\n")
self.assertEqual(got["new_endpoint_pins"], [])
self.assertFalse(got["audience"]["new"])
self.assertFalse(got["egress_namespace"]["new"])
def test_env_names_are_not_runtime_audiences(self):
got = self.classify("url: http://access-engine-example.access-engine.svc.cluster.local:8080\nOWNER_ACCESS_ENGINE_URL: x\nmount: /var/run/secrets/access-engine-caller/token\nkind: NetworkPolicy\nspec: {egress: [{to: [{namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: access-engine}}}]}]}\n")
self.assertFalse(got["audience"]["new"])
self.assertEqual(got["phase"], "NEEDS_OWNER_SOURCE_WORK")
def test_missing_and_symlink_sources_refuse(self):
with tempfile.TemporaryDirectory() as temp:
root=Path(temp); c=migration.Consumer("owner", ("owner",), ("example",), ("runtime.yaml",), True)
got=migration.MigrationReadiness(root).classify(c)
self.assertEqual(got["sources"][0]["state"], "missing")
self.assertFalse(got["native_ready"])
(root/"runtime.yaml").symlink_to("/etc/hosts")
with self.assertRaises(ValueError): migration.MigrationReadiness(root).classify(c)
def test_deterministic_output_and_no_source_values_leak(self):
got=self.classify("secret: TEST_ONLY_MUST_NOT_EMIT\nurl: http://flex-auth-example.flex-auth.svc:8080\naudience: flex-auth\n")
self.assertNotIn("TEST_ONLY_MUST_NOT_EMIT", str(got))
self.assertEqual(len(got["sources"][0]["sha256"]), 64)
self.assertEqual(got["old_endpoint_pins"], ["example"])
if __name__ == "__main__": unittest.main()

View file

@ -0,0 +1,53 @@
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class PublicationTests(unittest.TestCase):
def run_publisher(self, mode='paired', revision='a' * 40, mismatch=False, array=False):
with tempfile.TemporaryDirectory() as folder:
base=Path(folder); log=base/'commands.jsonl'
docker=base/'docker'
docker.write_text('''#!/usr/bin/env python3
import json,os,sys
with open(os.environ['COMMAND_LOG'],'a') as f:f.write(json.dumps(sys.argv[1:])+'\\n')
if sys.argv[1:3]==['image','inspect']:
digest='sha256:'+('b' if os.environ.get('MISMATCH')=='1' and '/access-engine:' in sys.argv[-1] else 'a')*64
payload=[sys.argv[-1].split(':main-')[0]+'@'+digest]
print(json.dumps({'invalid':'response'} if os.environ.get('ARRAY')=='1' else payload))
''');docker.chmod(0o755)
result=subprocess.run(['bash',str(ROOT/'tools/publish-runtime-images.sh'),revision,mode,'.'],capture_output=True,text=True,env={**os.environ,'PATH':str(base)+':'+os.environ['PATH'],'COMMAND_LOG':str(log),'MISMATCH':str(int(mismatch)),'ARRAY':str(int(array))})
calls=[json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
return result,calls
def test_paired_builds_once_and_verifies_both_registry_digests(self):
result,calls=self.run_publisher();self.assertEqual(result.returncode,0,result.stderr)
self.assertEqual(sum(c[0]=='build' for c in calls),1)
pushes=[c[1] for c in calls if c[0]=='push']
self.assertEqual(set(pushes),{f'forgejo.coulomb.social/coulomb/{name}:{tag}' for name in ('access-engine','flex-auth') for tag in ('latest','main-aaaaaaa')})
self.assertEqual(sum(c[:2]==['image','inspect'] for c in calls),2)
self.assertEqual(sum(c[0]=='pull' for c in calls),2)
self.assertIn('manifest_digest=sha256:',result.stdout)
self.assertFalse(any(c[0]=='login' for c in calls))
def test_normal_push_cannot_publish_new_package(self):
result,calls=self.run_publisher('legacy');self.assertEqual(result.returncode,0)
self.assertFalse(any('coulomb/access-engine' in arg for call in calls for arg in call))
def test_different_registry_digest_refuses_success(self):
result,calls=self.run_publisher(mismatch=True);self.assertNotEqual(result.returncode,0)
self.assertNotIn('verified_repositories=',result.stdout)
def test_unexpected_inspection_response_refuses_success(self):
result,_=self.run_publisher(array=True);self.assertNotEqual(result.returncode,0)
def test_invalid_mode_or_mutable_revision_stops_before_docker(self):
for mode,revision in [('other','a'*40),('paired','main'),('paired','a'*7)]:
with self.subTest(mode=mode,revision=revision):
result,calls=self.run_publisher(mode,revision);self.assertNotEqual(result.returncode,0);self.assertEqual(calls,[])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Uses the enclosing Forge runner's existing registry login. No credential reads.
set -euo pipefail
revision=${1:?full source revision required}
mode=${2:-legacy}
context=${3:-.}
[[ "$revision" =~ ^[a-f0-9]{40}$ ]] || { echo 'full immutable source revision required' >&2; exit 2; }
[[ "$mode" == legacy || "$mode" == paired ]] || { echo 'unknown publication mode' >&2; exit 2; }
short=${revision:0:7}
registry=forgejo.coulomb.social
legacy="$registry/coulomb/flex-auth"
renamed="$registry/coulomb/access-engine"
docker build -f "$context/Containerfile" --label "org.opencontainers.image.revision=$revision" -t "$legacy:main-$short" "$context"
images=("$legacy")
if [[ "$mode" == paired ]]; then
docker tag "$legacy:main-$short" "$renamed:main-$short"
images+=("$renamed")
fi
for image in "${images[@]}"; do
docker tag "$image:main-$short" "$image:latest"
docker push "$image:main-$short"
docker push "$image:latest"
done
# Pull each published immutable tag through the runner's existing registry login.
# RepoDigests then records the registry's top-level digest, including OCI indexes
# and attestations; comparing a local image/config ID would not prove publication.
digests=()
for image in "${images[@]}"; do
docker pull "$image:main-$short"
raw=$(docker image inspect --format '{{json .RepoDigests}}' "$image:main-$short")
digest=$(printf '%s' "$raw" | python3 -c 'import json,re,sys; x=json.load(sys.stdin); assert isinstance(x,list); matches=[v.removeprefix(sys.argv[1]+"@") for v in x if isinstance(v,str) and v.startswith(sys.argv[1]+"@")]; assert len(matches)==1 and re.fullmatch(r"sha256:[a-f0-9]{64}",matches[0]); print(matches[0])' "$image")
digests+=("$digest")
done
if [[ "$mode" == paired && "${digests[0]}" != "${digests[1]}" ]]; then
echo 'published registry manifest digests differ' >&2; exit 1
fi
printf 'publication mode=%s revision=%s manifest_digest=%s verified_repositories=%s\n' "$mode" "$revision" "${digests[0]}" "${#images[@]}"

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Deterministic T06 source classification. No credentials, network or mutation.
This reports source preparation, NEVER native readiness or workplan closure.
Owner-authenticated live allow/deny, predecessor and no-traffic/soak evidence
must be independently collected and reviewed by the migration coordinator.
"""
from dataclasses import dataclass
from pathlib import Path
import argparse
import ast
import tomllib
import yaml
import hashlib
import json
import re
from urllib.parse import urlparse
@dataclass(frozen=True)
class Consumer:
owner: str
repos: tuple
pins: tuple
paths: tuple
network_policy: bool
CONSUMERS = (
Consumer("ops-warden", ("ops-warden", "ops-bridge"), ("ops-warden", "ops-warden-fetch"), ("ops-warden/examples/warden.production.example.yaml",), False),
Consumer("secrets-engine", ("secrets-engine",), ("secrets-engine",), ("secrets-engine/tools/exercise_approval_identity.py",), False),
Consumer("user-engine", ("user-engine", "net-kingdom"), ("user-engine", "application-readback"), ("net-kingdom/sso-mfa/k8s/user-engine/runtime.yaml",), True),
Consumer("tenant-engine", ("tenant-engine",), ("tenant-engine",), ("tenant-engine/deploy/base/tenant-engine.yaml", "tenant-engine/railiance/app.toml"), True),
Consumer("informed-decision", ("informed-decision",), ("informed-decision-lifecycle", "informed-decision-sitting", "informed-decision-t03"), ("informed-decision/deploy/sitting-admission.json", "informed-decision/deploy/t03-admission.json", "informed-decision/deploy/lifecycle/admission.candidate.json", "informed-decision/deploy/lifecycle/service.candidate.json", "informed-decision/tools/render_deployment.py"), True),
)
class MigrationReadiness:
def __init__(self, estate_root):
self.root = Path(estate_root).resolve()
def classify(self, consumer):
sources = []; texts = []; documents = []
for relative in consumer.paths:
path = self.root / relative
# Never follow a redirected public path into a secret/private tree.
if path.is_symlink() or (path.exists() and not path.resolve().is_relative_to(self.root)):
raise ValueError("public source path escapes bounded estate root")
if not path.is_file():
sources.append({"path": relative, "state": "missing"}); continue
raw = path.read_bytes(); text = raw.decode()
source = {"path": relative, "state": "present", "sha256": hashlib.sha256(raw).hexdigest()}
try:
if path.suffix == ".py":
# Literal configuration strings only, never execute source.
values = [n.value for n in ast.walk(ast.parse(text)) if isinstance(n, ast.Constant) and isinstance(n.value, str)]
texts.extend(values)
for value in values:
try:
documents.extend(doc for doc in yaml.safe_load_all(value) if isinstance(doc, (dict, list)))
except yaml.YAMLError:
pass
elif path.suffix == ".toml":
doc = tomllib.loads(text); documents.append(doc)
texts.append(json.dumps(doc, sort_keys=True))
else:
# JSON is a YAML subset; parse documents to exclude comments.
docs = [doc for doc in yaml.safe_load_all(text) if doc is not None]
documents.extend(docs)
texts.extend(json.dumps(doc, sort_keys=True) for doc in docs)
except (ValueError, SyntaxError, yaml.YAMLError):
source["state"] = "invalid"
sources.append(source)
text = "\n".join(texts)
old_urls = []; new_urls = []
endpoints = []
for pin in consumer.pins:
old = f"flex-auth-{pin}.flex-auth.svc"
new = f"access-engine-{pin}.access-engine.svc"
observed = []
for value in re.findall(r"https?://[^\s\"'<>]+", text):
try:
observed.append(urlparse(value.rstrip(".,;")))
except ValueError:
continue # templates/malformed URLs are not endpoint evidence
def has_endpoint(host):
for value in observed:
try:
if value.hostname in (host, host + ".cluster.local") and value.port == 8080:
return True
except ValueError:
continue
return False
if has_endpoint(old): old_urls.append(pin)
if has_endpoint(new): new_urls.append(pin)
endpoints.append({"pin": pin, "old_origin": f"http://{old}.cluster.local:8080", "new_origin": f"http://{new}.cluster.local:8080", "audience": "access-engine"})
# Only runtime audience and namespace-selector literals count. Env names,
# caller mount paths, prose and historical package names do not.
audiences = set(); namespaces = set()
def collect(value):
if isinstance(value, dict):
audience = value.get("audience")
if isinstance(audience, str): audiences.add(audience)
if value.get("kind") == "NetworkPolicy":
spec = value.get("spec", {})
for rule in spec.get("egress", []) if isinstance(spec, dict) else []:
if not isinstance(rule, dict): continue
for peer in rule.get("to", []):
if not isinstance(peer, dict): continue
selector = peer.get("namespaceSelector", {})
labels = selector.get("matchLabels", {}) if isinstance(selector, dict) else {}
if isinstance(labels, dict) and isinstance(labels.get("kubernetes.io/metadata.name"), str):
namespaces.add(labels["kubernetes.io/metadata.name"])
for child in value.values(): collect(child)
elif isinstance(value, list):
for child in value: collect(child)
for doc in documents: collect(doc)
audience_old = "flex-auth" in audiences
audience_new = "access-engine" in audiences
egress_old = "flex-auth" in namespaces
egress_new = "access-engine" in namespaces
reasons = []
if any(s["state"] != "present" for s in sources): reasons.append("inventory source files missing/invalid; owner must reconcile")
if len(new_urls) != len(consumer.pins): reasons.append("not every new pin endpoint appears in bounded public source")
if not audience_new: reasons.append("new projected-token audience not observed in public source")
if consumer.network_policy and not egress_new: reasons.append("new namespace-selector egress not observed; add before switching")
if old_urls and consumer.network_policy and egress_new and not egress_old: reasons.append("old endpoint remains but old namespace-selector removed; unsafe migration order")
if reasons: phase = "NEEDS_OWNER_SOURCE_WORK"
elif old_urls or audience_old: phase = "SOURCE_DUAL_PREPARATION_REVIEW_REQUIRED"
else: phase = "SOURCE_SWITCHED_NATIVE_PROOF_REQUIRED"
return {"owner": consumer.owner, "source_owners": list(consumer.repos), "phase": phase, "native_ready": False, "sources": sources, "old_endpoint_pins": old_urls, "new_endpoint_pins": new_urls, "audience": {"old": audience_old, "new": audience_new}, "egress_namespace": {"old": egress_old, "new": egress_new}, "reasons": reasons, "handoff": {"endpoints": endpoints, "order": ["verify T02/T05 and ordered predecessor live proof", "owner adds new egress while retaining old", "owner switches URL and projected audience for one consumer", "prove live allow and deny against every new pin", "observe old-twin logs with bounded no-traffic/soak evidence", "owner removes old egress only after evidence review"], "required_live_evidence": ["owner identity and exact deployed revision", "old/new image digest and package correspondence", "authenticated allow and deny exact decision bindings", "old-twin log observation interval and no-traffic results; silence/source strings alone are insufficient"], "special": "ops-warden operator config and ops-bridge ClusterIP tunnels require owner evidence; no new self resource.system grant may be inferred from policy routing lane" if consumer.owner == "ops-warden" else "CLI/operator runtime config requires owner evidence" if consumer.owner == "secrets-engine" else "public source does not establish live deployment"}}
def report(self):
return {"schema": "runtime-consumer-migration-source-v1", "workplan": "FLEX-WP-0036", "task": "FLEX-WP-0036-T06", "scope": "deterministic public-source readiness classification, no native admission", "dependency_state": "T02/T05 and ordered live proof require coordinator verification; this tool cannot assert completion", "consumers": [self.classify(c) for c in CONSUMERS]}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--estate-root", type=Path, default=Path(__file__).resolve().parents[2])
args = parser.parse_args()
print(json.dumps(MigrationReadiness(args.estate_root).report(), indent=2, sort_keys=True))
if __name__ == "__main__": main()

View file

@ -65,7 +65,9 @@ written.
```task
id: FLEX-WP-0036-T01
status: progress
status: wait
needs_human: true
intervention_note: "Exact new runtime principals/naming are included in the single daily red batch with the nine TokenReview grants; no per-item question."
priority: high
state_hub_task_id: "4f9a796d-832f-5936-8a34-eb980f6e1b57"
```
@ -82,6 +84,8 @@ Gate: decision accepted by the founder.
`flex-auth` is a stub that prints the rename and exits 2. The other target
names in the table still need the founder's acceptance.
2026-10-11: target names are captured as proposed FLEX-DEC-2026-021. The new reviewer subjects are the scope-expansion component and are already in the single daily red batch. Binary disposition FLEX-DEC-2026-020 remains accepted; no founder acceptance of this broader table is fabricated.
## Inventory every consumer reference and open handoffs
```task
@ -115,9 +119,9 @@ current source must now come from `./cmd/access-engine`.
```task
id: FLEX-WP-0036-T03
status: progress
status: wait
priority: high
depends_on: [FLEX-WP-0036-T01]
depends_on: [FLEX-WP-0036-T01, FLEX-WP-0036-T04, RPF-WP-0044-T07]
state_hub_task_id: "487525e5-c89e-586e-8e9b-54ac6da0804e"
```
@ -144,11 +148,15 @@ PDP-self resource lane in these pin packages; the old inventory's
fetch package. No new self-lane or caller grant is created to fill that absence.
T03 remains progress until the new image and ordered live checks are proven.
2026-10-11: source compatibility is tested and locally built. The next native step waits for the accepted runtime table, published exact artifact and the ordered Argo ownership proof (RPF-WP-0044-T07). No new founder rollout question; the source/public-package scope lives in the existing single batch.
## Rename the source
```task
id: FLEX-WP-0036-T04
status: progress
status: wait
needs_human: true
intervention_note: "New public coulomb/access-engine image package publication is included in the single daily red batch. Local source/build checks pass; paired publication has not run."
priority: medium
depends_on: [FLEX-WP-0036-T01]
state_hub_task_id: "ff7fd369-326f-5044-aaa4-96c5f45c80e9"
@ -177,6 +185,8 @@ which are immutable on a `helm upgrade`, so it ships with the new releases.
Also the image package and the `/opt/flex-auth` paths, which every live pin's
args reference.
2026-10-11: source now includes canonical `/opt/access-engine/examples` and retains the legacy paths for existing pins. The lifecycle Containerfile starts the renamed engine rather than the retired stub. The old Topaz integration variable remains usable for one release with a warning; an explicit new value takes precedence. CI builds once, publishes to the already admitted legacy package on normal push, and has an explicit paired-publication dispatch for the new package. `tools/publish-runtime-images.sh` independently compares both registry manifest digests; a local image ID or equal config alone is not publication proof. The new public package is a daily red-batch item; no paired dispatch has run. T04 remains progress until accepted publication resolves both repositories to the same immutable digest.
## Stand up the access-engine namespace next to flex-auth
```task
@ -219,7 +229,7 @@ native action occurred. T03/T04 and ordered predecessor gates remain unchanged.
```task
id: FLEX-WP-0036-T06
status: todo
status: wait
priority: medium
depends_on: [FLEX-WP-0036-T02, FLEX-WP-0036-T05]
state_hub_task_id: "f0470e5c-3ea7-506c-b8bb-3ade0b57ff88"
@ -233,6 +243,15 @@ allow and a live deny against the new pin, and no traffic left on the twin
Gate: every inventory row closed with owner evidence.
2026-10-11 native dependency wait qualified once on existing T02/T05 and
ordered predecessor live proof; no consumer switch executed. Actual source work
adds a deterministic `MigrationReadiness` classifier and public-source
re-derivation/owner handoff report. All five owners still require source work;
operator and tunnel custody remains with their owners. The verifier refuses to
turn comments, spoofed hosts, unsigned readiness flags or source-only changes
into native readiness. Owner-authenticated live allow/deny and timed old-twin
no-traffic/soak evidence remain necessary; this task is not closed by the report.
## Retire the flex-auth runtime
```task