railiance-apps/docs/informed-decision-origin.md
tegwick eff457ef52
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 12s
docs: correct stale key-cape blocker claim on the decisions origin
An earlier version said the origin unblocks KEY-WP-0013-T02, 'blocked since
2026-09-08'. Both halves are wrong. T02 is done, and so is T05 -- the task that
actually held the human registration. key-cape published the contract shape from
its side and closed a follow-up informed-decision prompted (KEY-WP-0030).
Nothing there waits on this host.

The error came from repeating informed-decision's workplan prose of 2026-09-09
without checking the source repo. The origin work stands on its own: T07's
acceptance requires a real origin regardless of who else is waiting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJbh7o7UWF4tQ5jxygnNGu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2072522@bnt-lap001
Assistant-Session: 46173adf-7302-4ede-99d6-963b61359928
2026-09-11 00:53:08 +02:00

175 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# decisions.coulomb.social — origin for the informed-decision approver surface
**Status:** applied and live. `https://decisions.coulomb.social/` answers 200
over a valid Let's Encrypt certificate as of 2026-09-10 14:32 UTC.
Last reviewed: 2026-09-10
## Why this host exists before the app does
`informed-decision` owns the browser-facing approver UI that `approval-engine`
deliberately does not contain. Its workplan task `INFD-WP-0001-T07` must publish two strings to `key-cape` — an
OIDC `client_id` and a full callback URI — and cannot do so against a host that
does not answer.
**Correction, 2026-09-11.** An earlier version of this document said the origin
unblocks `key-cape`'s `KEY-WP-0013-T02`, "blocked since 2026-09-08". That is
wrong twice over, and the error came from repeating `informed-decision`'s
workplan prose of 2026-09-09 without checking the source repo. `KEY-WP-0013-T02`
is `done`; so is `T05`, the task that actually carried the human registration.
`key-cape` published the contract shape from its side — including the
`approval:read` scope `informed-decision` asked for and a fix to the `assurance`
object's `at` field — and closed a follow-up (`KEY-WP-0030`) that
`informed-decision` prompted. **Nothing in `key-cape` is waiting on this host.**
What remains is `informed-decision`'s own `T07`, whose acceptance requires the
real origin regardless.
Both strings are now fixed except for the host:
| Field | Value |
| --- | --- |
| `client_id` | `informed-decision-approver` |
| Redirect URI | `https://decisions.coulomb.social/auth/callback` |
| Flow | authorization code + S256 PKCE, public client |
| Scopes | `openid`, `approval:read`, `approval:approve` |
Redirect URIs match byte-exactly at `/authorize`. A registration pointing at a
host that does not answer fails closed at first login and presents as a rejected
approval rather than as a registration defect — which is exactly the failure
`approval-engine` avoided by refusing to invent these strings. So the origin has
to be real before the registration is submitted, and that is S5 work here rather
than in `informed-decision`.
The approver UI itself is `INFD-WP-0001-T08`, still gated on `approval-engine`
`APPROVAL-WP-0002-T01` and on intake `INFD-IN-0003` (the evidence copy must reach
`audit-core` independently of this component). This host therefore ships a
placeholder first and the real surface later.
## What is in the repo
| File | Contents |
| --- | --- |
| `manifests/informed-decision-origin.yaml` | Namespace, placeholder nginx ConfigMap/Deployment/Service on `informed-decision:80` |
| `manifests/informed-decision-ingress.yaml` | Traefik Ingress + `letsencrypt-prod` certificate for `decisions.coulomb.social` |
| `manifests/informed-decision-http-redirect.yaml` | `redirectScheme` Middleware + `web`-entrypoint Ingress, HTTP → HTTPS |
The placeholder is `nginxinc/nginx-unprivileged`, read-only root filesystem,
non-root, `noindex`. When the real surface lands it takes over the same Service
name and the origin file shrinks to the Namespace.
## Deliberate single path rule
The Ingress carries exactly one rule: `/` Prefix to one backend.
`reuse-surface` reported on 2026-07-07 that an Ingress declaring an `Exact`
`/health` rule alongside a catch-all `/` Prefix rule on the same host had the
exact match swallowed by the catch-all — the public health URL returned 404 from
the landing container while the pod was `1/1 Ready` and its own probes passed.
Splitting `/auth`, `/api` and `/` across backends here would reproduce that on a
host whose whole purpose is an exact-match redirect URI. If a second backend ever
becomes necessary, set `traefik.ingress.kubernetes.io/router.priority`
explicitly rather than relying on rule order.
## Preconditions verified 2026-09-10
- DNS `decisions.coulomb.social``92.205.62.239` (same A record as `reuse`).
- `letsencrypt-prod` ClusterIssuer `Ready=True`.
- No existing Ingress claims `decisions.coulomb.social`.
- Server-side dry-run of all five objects against the live API is clean.
The namespaced four were validated against an existing namespace, since a
server dry-run cannot create the new one first; see
`DRY_RUN_CREATE_NAMESPACES` in `tools/k8s-server-dry-run.sh`.
## Deployed 2026-09-10
Applied with operator approval. Evidence:
| Check | Result |
| --- | --- |
| Placeholder pod | `1/1 Running`, `/healthz``ok` in-pod |
| Certificate `informed-decision-tls` | `Ready=True`, ACME order `valid` |
| Issuer / subject | `CN=decisions.coulomb.social`, Let's Encrypt `YR2` |
| Validity | `2026-09-10``2026-12-09` (cert-manager renews) |
| `GET https://decisions.coulomb.social/` | `HTTP/2 200`, chain verify `0` |
| `GET https://decisions.coulomb.social/auth/callback` | `200` — the exact redirect URI resolves |
| `GET http://decisions.coulomb.social/` | `301`/`308` → HTTPS (added 2026-09-10) |
`/auth/callback` currently returns the placeholder page via the SPA `try_files`
fallback. That is the correct behaviour for now: the origin answers, which is
what the registration needs. The real surface will handle the path when
`INFD-WP-0001-T08` ships.
**`informed-decision` can now submit `docs/keycape-client-registration.md` to
`key-cape`.** That closes its own `INFD-WP-0001-T07`, not a `key-cape` task —
see the correction above.
## Redeploy / recovery
```bash
export KUBECONFIG=$HOME/.kube/config-hosteurope
kubectl apply -f manifests/informed-decision-origin.yaml
kubectl apply -f manifests/informed-decision-ingress.yaml
kubectl apply -f manifests/informed-decision-http-redirect.yaml
kubectl -n informed-decision get pods,svc,ingress
kubectl -n informed-decision get certificate informed-decision-tls -w
curl -sSI https://decisions.coulomb.social/ | head -1
```
That `curl` succeeded on 2026-09-10, which is the gate `informed-decision` was
waiting on.
**Note on the kubeconfig:** `~/.kube/config-hosteurope` names port `16443`, but
the `k3s-api-railiance01` ops-bridge tunnel currently listens on `16444`.
Override with `--server https://127.0.0.1:16444` or fix the kubeconfig; `bridge
check` reports the tunnel healthy either way, so it does not surface the
mismatch.
## HTTP → HTTPS
`manifests/informed-decision-http-redirect.yaml` adds a `redirectScheme`
Middleware plus a `web`-entrypoint Ingress, following the `reuse-surface`
pattern. Applied 2026-09-10.
`http://decisions.coulomb.social/` now returns `308` to HEAD and `301` to GET.
That split is not a defect in this configuration — `reuse.coulomb.social`, which
uses the identical Middleware, behaves the same way. It is Traefik's own
behaviour on this cluster.
The main Ingress stays `websecure`-only; this is a second router on port 80
rather than a change to the first.
### Router priority is load-bearing here
cert-manager solves HTTP-01 by creating a temporary solver Ingress on **this
same host and this same `web` entrypoint**, serving
`/.well-known/acme-challenge/<token>`. A catch-all `/` redirect on that
entrypoint competes with it directly.
Traefik would normally settle this by rule length — the solver's `Path` rule is
much longer than `PathPrefix(/)` — but `reuse-surface`'s 2026-07-07 report is
precisely a case of a specific rule losing to a catch-all when precedence was
left implicit. There the symptom was a 404 someone noticed within the day. Here
it would be a **silently failed renewal about 60 days out**, surfacing as an
expired certificate on the origin backing an OIDC redirect URI — the kind of
failure that presents as a broken login rather than as an expired cert.
So the redirect router pins `traefik.ingress.kubernetes.io/router.priority: "1"`.
Any solver router, using the default computed priority, outbids it.
**Verified by probe, twice, on 2026-09-10.** A throwaway Ingress mimicking the
solver shape (`web` entrypoint, `Exact` acme-challenge path, default priority)
was applied and the challenge path was polled:
| Condition | `/.well-known/acme-challenge/<token>` | `/` |
| --- | --- | --- |
| No solver present | `301` (redirect catches it — correct) | `301` |
| Solver present | `200` (**solver wins**) | `301` |
| Solver removed | `301` | `301` |
Traefik takes 3060s to converge on Ingress changes, so the flip is not
instant; poll rather than reading a single response. The probe was deleted after
each run and is not in the repo — recreate it from this table if the redirect
or the priority annotation is ever changed.
The next real renewal is due around 2026-11-09. Confirm the certificate's
`notAfter` advances then; that is the only test that exercises the real solver
rather than a probe of its shape.