diff --git a/.custodian-brief.md b/.custodian-brief.md index 33e5176..dd1967f 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,7 +2,7 @@ # Custodian Brief — core-hub **Domain:** infotech -**Last synced:** 2026-07-03 16:36 UTC +**Last synced:** 2026-07-02 16:13 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams @@ -16,6 +16,14 @@ Progress: 0/4 done | workstream_id: `a8d66822-e435-4b1e-ad81-37a298d1795e` - ! Update Cross-Repo References `ed7f8e01` - · Retire Probe Repo `d3a494a3` +### Migrate Inter-Hub data and cut over production +Progress: 1/4 done | workstream_id: `a36efc08-5906-40c7-b1de-f3a553ffea02` + +**Open tasks:** +- ! Run Staging Import `e0ea0928` +- ! Dual-Run Smokes `9166afe8` +- ! Production Cutover `fa68858a` + --- ## MCP Orientation (when available) diff --git a/.forgejo/workflows/ci-smoke.yaml b/.forgejo/workflows/ci-smoke.yaml deleted file mode 100644 index bd44c56..0000000 --- a/.forgejo/workflows/ci-smoke.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Canonical CI smoke template (tier 1 routing drill). -# Copy to: .forgejo/workflows/ci-smoke.yaml in consumer repos. -name: CI Smoke - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - host-smoke: - runs-on: self-hosted - steps: - - name: Routing probe (host runner) - run: | - set -eu - echo "repository=${GITHUB_REPOSITORY:-unknown}" - echo "sha=${GITHUB_SHA:-unknown}" - echo "runner=${RUNNER_NAME:-unknown}" - uname -a - - container-smoke: - runs-on: ubuntu-latest - steps: - - name: Routing probe (container label) - run: | - set -eu - echo "container-smoke ok for ${GITHUB_REPOSITORY:-unknown}" \ No newline at end of file diff --git a/.forgejo/workflows/image.yaml b/.forgejo/workflows/image.yaml deleted file mode 100644 index fb26fba..0000000 --- a/.forgejo/workflows/image.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build and Publish Container Image - -on: - push: - branches: - - main - paths: - - ".forgejo/workflows/image.yaml" - - "Dockerfile" - - "src/**" - - "migrations/**" - - "scripts/**" - - "pyproject.toml" - - "uv.lock" - - "alembic.ini" - workflow_dispatch: - -env: - REGISTRY: forgejo.coulomb.social - IMAGE_NAME: coulomb/core-hub - DOCKER_HOST: tcp://127.0.0.1:2375 - -jobs: - build-and-push: - runs-on: container-build - steps: - - name: Build and push image - env: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -eu - REF="${GITHUB_SHA:-main}" - SHORT="${REF:0:7}" - mkdir -p buildctx "${HOME}/bin" - wget -qO /tmp/repo.tar.gz \ - "https://forgejo.coulomb.social/${GITHUB_REPOSITORY}/archive/${SHORT}.tar.gz" - tar xzf /tmp/repo.tar.gz -C buildctx --strip-components=1 - wget -qO- https://download.docker.com/linux/static/stable/x86_64/docker-27.3.1.tgz \ - | 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 -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}" \ No newline at end of file diff --git a/k8s/railiance-production/00-namespace.yaml b/k8s/railiance-production/00-namespace.yaml deleted file mode 100644 index aa823a7..0000000 --- a/k8s/railiance-production/00-namespace.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: core-hub - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/part-of: core-hub - environment: production - railiance.io/postgres-client: apps-pg diff --git a/k8s/railiance-production/20-runtime.yaml b/k8s/railiance-production/20-runtime.yaml deleted file mode 100644 index f19a822..0000000 --- a/k8s/railiance-production/20-runtime.yaml +++ /dev/null @@ -1,113 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: core-hub-prod-runtime - namespace: core-hub - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/part-of: core-hub - environment: production -data: - CORE_HUB_ENV: production - CORE_HUB_AUTO_CREATE_TABLES: "0" ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: core-hub-prod-migrate - namespace: core-hub - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: migration - app.kubernetes.io/part-of: core-hub - environment: production -spec: - backoffLimit: 1 - template: - metadata: - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: migration - app.kubernetes.io/part-of: core-hub - environment: production - spec: - restartPolicy: Never - containers: - - name: migrate - image: gitea.coulomb.social/coulomb/core-hub:production-placeholder - imagePullPolicy: IfNotPresent - command: ["alembic", "upgrade", "head"] - envFrom: - - configMapRef: - name: core-hub-prod-runtime - - secretRef: - name: core-hub-prod-env ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: core-hub-api - namespace: core-hub - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: api - app.kubernetes.io/part-of: core-hub - environment: production -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: api - template: - metadata: - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: api - app.kubernetes.io/part-of: core-hub - environment: production - spec: - containers: - - name: api - image: gitea.coulomb.social/coulomb/core-hub:production-placeholder - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 8010 - envFrom: - - configMapRef: - name: core-hub-prod-runtime - - secretRef: - name: core-hub-prod-env - readinessProbe: - httpGet: - path: /readyz - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /healthz - port: http - initialDelaySeconds: 15 - periodSeconds: 20 ---- -apiVersion: v1 -kind: Service -metadata: - name: core-hub-api - namespace: core-hub - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: api - app.kubernetes.io/part-of: core-hub - environment: production -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: core-hub - app.kubernetes.io/component: api - ports: - - name: http - port: 8010 - targetPort: http diff --git a/k8s/railiance-production/30-ingress.yaml b/k8s/railiance-production/30-ingress.yaml deleted file mode 100644 index 454e299..0000000 --- a/k8s/railiance-production/30-ingress.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: core-hub - namespace: core-hub - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - traefik.ingress.kubernetes.io/router.entrypoints: websecure - traefik.ingress.kubernetes.io/router.tls: "true" - labels: - app.kubernetes.io/name: core-hub - app.kubernetes.io/part-of: core-hub - environment: production -spec: - ingressClassName: traefik - rules: - - host: hub.coulomb.social - http: - paths: - - backend: - service: - name: core-hub-api - port: - number: 8010 - path: / - pathType: Prefix - tls: - - hosts: - - hub.coulomb.social - secretName: core-hub-tls diff --git a/k8s/railiance-production/README.md b/k8s/railiance-production/README.md deleted file mode 100644 index 8e51392..0000000 --- a/k8s/railiance-production/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Core Hub production deployment (hub.coulomb.social cutover) - -`30-ingress.yaml` is the cutover flip — do NOT apply it while the inter-hub -ingress still claims `hub.coulomb.social`. - -## Sequence - -```bash -# 1. Image (operator: registry push) -IMAGE_REPOSITORY=gitea.coulomb.social/coulomb/core-hub IMAGE_TAG= make container-build -docker push gitea.coulomb.social/coulomb/core-hub: - -# 2. Prod DB + secret (operator: shared-DB DDL) -# role core_hub / db core_hub on apps-pg; secret core-hub-prod-env with -# CORE_HUB_DATABASE_URL (postgresql+asyncpg://...apps-pg-rw.databases...:5432/core_hub) -# and CORE_HUB_API_TOKEN (generated) - -# 3. Deploy (no traffic yet) -kubectl apply -f k8s/railiance-production/00-namespace.yaml -kubectl apply -f k8s/railiance-production/20-runtime.yaml -kubectl -n core-hub set image job/core-hub-prod-migrate migrate=gitea.coulomb.social/coulomb/core-hub: -kubectl -n core-hub set image deploy/core-hub-api api=gitea.coulomb.social/coulomb/core-hub: -kubectl -n core-hub wait --for=condition=complete job/core-hub-prod-migrate --timeout=180s -kubectl -n core-hub rollout status deploy/core-hub-api --timeout=180s - -# 4. Final data import (fresh Inter-Hub export at cutover time) -kubectl exec -i -n databases net-kingdom-pg-1 -- psql -U postgres -d interhub -tA \ - < scripts/migration/inter_hub_export.sql > final-bundle.json -# validate, then import in-cluster (see scripts/migration/README.md) - -# 5. THE FLIP (operator approval point) -kubectl -n inter-hub delete ingress inter-hub -kubectl apply -f k8s/railiance-production/30-ingress.yaml -# cert-manager issues core-hub-tls for hub.coulomb.social (~1 min) - -# 6. Verify -curl -s https://hub.coulomb.social/healthz # 200 -curl -s https://hub.coulomb.social/api/v2/widget-types | head -c 200 -curl -s -o /dev/null -w '%{http_code}' https://hub.coulomb.social/api/v2/hubs # 401 = hardened contract - -## Rollback (any time; Inter-Hub deployment stays running) -kubectl delete -f k8s/railiance-production/30-ingress.yaml -kubectl -n inter-hub apply -f # or helm upgrade inter-hub -``` - -Keep the Inter-Hub deployment/service running untouched until stabilization -passes (CORE-WP-0005-T04); only the ingress moves. diff --git a/k8s/railiance-production/rollback-inter-hub-ingress.yaml b/k8s/railiance-production/rollback-inter-hub-ingress.yaml deleted file mode 100644 index b1d7628..0000000 --- a/k8s/railiance-production/rollback-inter-hub-ingress.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - meta.helm.sh/release-name: inter-hub - meta.helm.sh/release-namespace: inter-hub - traefik.ingress.kubernetes.io/router.entrypoints: websecure - traefik.ingress.kubernetes.io/router.tls: "true" - creationTimestamp: "2026-06-14T12:49:01Z" - generation: 1 - labels: - app: inter-hub - app.kubernetes.io/managed-by: Helm - name: inter-hub - namespace: inter-hub - resourceVersion: "14479609" - uid: 2f1d1557-76be-445c-b77b-e03e7b0c3935 -spec: - ingressClassName: traefik - rules: - - host: hub.coulomb.social - http: - paths: - - backend: - service: - name: inter-hub - port: - number: 8000 - path: / - pathType: Prefix - tls: - - hosts: - - hub.coulomb.social - secretName: inter-hub-tls -status: - loadBalancer: - ingress: - - ip: 92.205.130.254 diff --git a/src/core_hub/seeds.py b/src/core_hub/seeds.py index b94bd6a..bf78096 100644 --- a/src/core_hub/seeds.py +++ b/src/core_hub/seeds.py @@ -35,69 +35,12 @@ WIDGET_TYPES = [ CatalogItem(slug="status-summary", name="Status Summary"), CatalogItem(slug="workplan-board", name="Workplan Board"), CatalogItem(slug="event-stream", name="Event Stream"), - # Inter-Hub compatibility vocabulary (CORE-WP-0005-T04 prerequisite 1): - # the full legacy widget_type_registry so migrated widgets stay - # referentially complete and legacy catalog consumers see a superset. - CatalogItem(slug="action", name="Action Control", description="Button, link, or trigger widget"), - CatalogItem(slug="chart", name="Chart", description="Data visualisation chart widget"), - CatalogItem(slug="chat", name="Chat Region", description="Conversational interaction region"), - CatalogItem(slug="diff", name="Diff / Review", description="Code diff or change review element"), - CatalogItem(slug="form", name="Form", description="Data entry form widget"), - CatalogItem(slug="ops-backup-set", name="ops-backup-set"), - CatalogItem(slug="ops-cluster", name="ops-cluster"), - CatalogItem(slug="ops-endpoint", name="ops-endpoint"), - CatalogItem(slug="ops-environment", name="ops-environment"), - CatalogItem(slug="ops-host", name="ops-host"), - CatalogItem(slug="ops-incident", name="ops-incident"), - CatalogItem(slug="ops-migration-wave", name="ops-migration-wave"), - CatalogItem(slug="ops-readiness-gate", name="ops-readiness-gate"), - CatalogItem(slug="ops-release", name="ops-release"), - CatalogItem(slug="ops-risk", name="ops-risk"), - CatalogItem(slug="ops-runbook", name="ops-runbook"), - CatalogItem(slug="ops-secret-set", name="ops-secret-set"), - CatalogItem(slug="ops-service", name="ops-service"), - CatalogItem(slug="ops-service-catalog", name="ops-service-catalog"), - CatalogItem(slug="panel", name="Status Panel", description="Summary or status information panel"), - CatalogItem(slug="recommendation", name="Recommendation", description="AI or system recommendation block"), - CatalogItem(slug="table", name="Table", description="Tabular data display widget"), - CatalogItem(slug="workflow-step", name="Workflow Step", description="Single step in a multi-step workflow"), ] EVENT_TYPES = [ CatalogItem(slug="interaction.event", name="Interaction Event"), CatalogItem(slug="workplan.progress", name="Workplan Progress"), CatalogItem(slug="evidence.recorded", name="Evidence Recorded"), - # Inter-Hub compatibility vocabulary (see WIDGET_TYPES note). - CatalogItem(slug="abandoned", name="Abandoned"), - CatalogItem(slug="accepted_recommendation", name="Accepted Recommendation"), - CatalogItem(slug="blocked_by_policy", name="Blocked by Policy"), - CatalogItem(slug="clicked", name="Clicked"), - CatalogItem(slug="commented", name="Commented"), - CatalogItem(slug="escalated", name="Escalated"), - CatalogItem(slug="failed", name="Failed"), - CatalogItem(slug="flagged_confusing", name="Flagged Confusing"), - CatalogItem(slug="flagged_helpful", name="Flagged Helpful"), - CatalogItem(slug="focused", name="Focused"), - CatalogItem(slug="ops-backup-verified", name="ops-backup-verified"), - CatalogItem(slug="ops-drift-detected", name="ops-drift-detected"), - CatalogItem(slug="ops-endpoint-verified", name="ops-endpoint-verified"), - CatalogItem(slug="ops-health-checked", name="ops-health-checked"), - CatalogItem(slug="ops-inventory-registered", name="ops-inventory-registered"), - CatalogItem(slug="ops-inventory-updated", name="ops-inventory-updated"), - CatalogItem(slug="ops-migration-gate-failed", name="ops-migration-gate-failed"), - CatalogItem(slug="ops-migration-gate-passed", name="ops-migration-gate-passed"), - CatalogItem(slug="ops-readiness-gate-updated", name="ops-readiness-gate-updated"), - CatalogItem(slug="ops-release-observed", name="ops-release-observed"), - CatalogItem(slug="ops-restore-tested", name="ops-restore-tested"), - CatalogItem(slug="ops-risk-accepted", name="ops-risk-accepted"), - CatalogItem(slug="ops-risk-raised", name="ops-risk-raised"), - CatalogItem(slug="ops-runbook-executed", name="ops-runbook-executed"), - CatalogItem(slug="ops-service-discovered", name="ops-service-discovered"), - CatalogItem(slug="rejected_recommendation", name="Rejected Recommendation"), - CatalogItem(slug="retracted", name="Retracted"), - CatalogItem(slug="retried", name="Retried"), - CatalogItem(slug="submitted", name="Submitted"), - CatalogItem(slug="viewed", name="Viewed"), ] ANNOTATION_CATEGORIES = [ diff --git a/workplans/CORE-WP-0005-data-migration-and-cutover.md b/workplans/CORE-WP-0005-data-migration-and-cutover.md index 3f27817..4d8d61d 100644 --- a/workplans/CORE-WP-0005-data-migration-and-cutover.md +++ b/workplans/CORE-WP-0005-data-migration-and-cutover.md @@ -47,7 +47,7 @@ actual import can run. ```task id: CORE-WP-0005-T03 -status: done +status: wait priority: high state_hub_task_id: "9166afe8-27ef-4345-a7b1-c33f15c90673" ``` @@ -60,46 +60,13 @@ Run public and protected smokes against both Inter-Hub and Core Hub. Record comp ```task id: CORE-WP-0005-T04 -status: done +status: wait priority: high state_hub_task_id: "fa68858a-b18e-4f09-a477-6b11e54099d4" ``` With operator approval, switch production traffic such as `hub.coulomb.social` to Core Hub. Keep rollback available until stabilization passes. -**Prerequisites (blocking, found in T03 dual-run 2026-07-02):** - -1. **Migrate/seed the ops-* type vocabularies.** Migrated Inter-Hub widgets - reference 7 widget types (`ops-endpoint`, `ops-environment`, `ops-host`, - `ops-migration-wave`, `ops-readiness-gate`, `ops-service`, - `ops-service-catalog`) plus their event types, none of which exist in Core - Hub's seeded registries. Either extend the `core-hub.migration.v1` bundle - with `widgetTypeRegistry` / `eventTypeRegistry` collections, or seed the - ops-* vocabularies into Core Hub before cutover, or migrated widgets are - referentially incomplete. -2. **Auth-posture change is a consumer break.** `/api/v2/hubs` is public on - Inter-Hub but protected on Core Hub — any anonymous consumer of the hub - list must gain a token before cutover. -3. Operator approval + rollback plan (existing gate). - -**Prerequisite resolutions 2026-07-03:** - -1. **Catalog gap — resolved by seeding.** Core Hub's catalogs are static - in-code seeds (`seeds.py`), not DB registries, so extending the migration - bundle schema was the wrong shape. Seeded the complete Inter-Hub - vocabulary instead (23 widget types incl. all 7 orphaned `ops-*` types, - 30 event types) as an explicit Inter-Hub-compatibility block; existing - Core Hub seeds retained. 22 tests pass. Migrated widgets are - referentially complete once the new image ships. -2. **Auth posture — resolved by keeping Core Hub protected.** Consumer - audit found only two `/api/v2/hubs` status consumers: - `ops-hub/interhub_gate_probe.py` **asserts 401** (it encodes the desired - hardened contract and currently fails against Inter-Hub production, - which serves 200), and `railiance-forge/tools/gitea-runner-status.sh` - is informational. No anonymous consumer depends on a 200. Core Hub's - protected listing is therefore an intentional hardening that satisfies - the ops-hub gate at cutover; no code change made. - ## Progress 2026-07-02 — staging import completed @@ -117,57 +84,3 @@ second run proved idempotency (created=0, updated=28, no duplicates). Server-side confirmed: the imported `ops-hub` hub and its 15 widgets are visible via `/api/v2/*`. T02 done; the staging DB now holds real Inter-Hub data for the T03 dual-run smokes. - - -## Progress 2026-07-02 — dual-run smokes and compatibility deltas - -Ran unauthenticated + authenticated probes against both live surfaces via -ops-bridge tunnels (Inter-Hub `inter-hub-coulombcore`:18020, Core Hub staging -`core-hub-staging-coulombcore`:18010). - -Status parity table (unauthenticated GET): - -| Endpoint | Inter-Hub | Core Hub | Note | -| --- | --- | --- | --- | -| `/healthz`, `/readyz` | 404 | 200 | Core Hub adds liveness/readiness (new capability, not a regression). | -| `/api/v2/hubs` | 200 | 401 | **Auth delta**: hub listing is public on Inter-Hub, protected on Core Hub. Anonymous consumers must authenticate against Core Hub. | -| `/api/v2/hub-registry` | 401 | 401 | Parity. | -| `/api/v2/widget-types`, `event-types`, `annotation-categories`, `policy-scopes` | 200 | 200 | Public-catalog status parity. | -| `/api/v2/widgets`, `interaction-events` | 401 | 401 | Parity. | - -Content/shape deltas: - -- Catalog record shape differs: Core Hub uses `{name, slug, description}`; - Inter-Hub uses `{name, label, description, ownerHubId, status}`. Consumers - parsing catalog records need updates for the `slug`-based shape. -- **Cutover blocker found**: the migrated Inter-Hub widgets reference 7 - widget-type names (`ops-endpoint`, `ops-environment`, `ops-host`, - `ops-migration-wave`, `ops-readiness-gate`, `ops-service`, - `ops-service-catalog`) that are **absent** from Core Hub's seeded - widget-type registry (which has only `Event Stream`, `Status Summary`, - `Workplan Board`). The `core-hub.migration.v1` bundle schema (COLLECTIONS) - carries widgets but not the `widget_type_registry` / `event_type_registry`, - so migrated widgets land referentially incomplete. **Production cutover - (T04) must first migrate or seed the ops-* widget-type and event-type - vocabularies into Core Hub.** Filed as the top T04 prerequisite. - -T03 done: compatibility surface is characterized; the auth-hardening delta and -the missing-catalog-vocabulary blocker are the two items the cutover decision -must account for. - - -## Completion 2026-07-03 — production cutover executed - -Operator-approved and operator-executed flip: `hub.coulomb.social` ingress -moved from Inter-Hub to Core Hub (`core-hub` namespace, image `a33191a`). -Verified post-flip: cert-manager issued `core-hub-tls`; `/healthz` and -`/readyz` 200; public catalogs 200 serving 26 widget types (all formerly -orphaned `ops-*` types present); `/api/v2/hubs` 401 — the hardened contract -the ops-hub gate probe asserts. Post-flip idempotent reconcile import: 0 -created / 28 updated / 0 errors (no Inter-Hub writes were missed). - -Rollback remains one command (`30-ingress.yaml` delete + -`rollback-inter-hub-ingress.yaml` apply) and the Inter-Hub deployment stays -running untouched through the stabilization window. `CORE-WP-0007` Haskell -retirement stays gated until stabilization passes and the operator approves -retiring the fallback.