--- id: FLEX-WP-0012 type: workplan title: "Authorize railiance-platform credential-grant requests" domain: infotech repo: flex-auth status: active owner: codex topic_slug: netkingdom planning_priority: P3 planning_order: 120 related_workplans: - RAILIANCE-WP-0005 created: "2026-08-11" updated: "2026-08-23" state_hub_workstream_id: "89ecdb1f-ceb0-4a50-b72d-c3dfd5fa7c73" --- # FLEX-WP-0012 - Authorize railiance-platform credential-grant requests `railiance-platform`'s credential broker (`scripts/credential.py`) can call flex-auth as a preflight before issuing a credential lease, posting non-secret grant metadata and expecting an allow/deny back. It has been able to do this since 2026-06-27. flex-auth has never answered, because nothing in this repo serves the endpoint the helper posts to or understands the payload it sends. Opened from State Hub capability request `893ff109`, filed 2026-07-02 and unanswered since. **Read the two corrections below before planning any work: the request as filed rests on a premise that is not true, and the urgency it describes has since evaporated.** ## Correction 1: this is not blocking anyone The capability request says `RAILIANCE-WP-0005-T07` is blocked. It is not. Later the same day, railiance-platform re-scoped and closed T07 on its own scope, and `RAILIANCE-WP-0005` is now `finished`. Their closure note records that the flex-auth call is optional by T07's own exit criteria, and that **the grant catalog already enforces TTL, actor-type, purpose, and delivery-mode bounds locally**. So this workplan buys *defence in depth and central, auditable policy* — a second, independently-owned opinion on credential issuance, with decision records — not the unblocking of stalled work. Priority is set accordingly. If it is never done, railiance-platform keeps enforcing the same bounds locally; what is lost is central policy and the decision log, not safety. Their closure note also says: "Decision taken autonomously (operator away); revert to `wait` if Bernd prefers to hold WP-0005 open on flex-auth." That choice is still open and should be confirmed rather than assumed. ## Correction 2: the stated technical blocker is wrong The request says the CARING `/v1/check` schema "cannot express TTL bounds, purpose, or delivery-mode checks." That is not correct, and the counter-example already ships in this repo. `examples/ops-warden/policy_package.md` denies `ttl_out_of_bounds` today by reading `input.context.ttl_hours` and comparing it against `input.resource.attributes.max_ttl_hours`. `CheckRequest` carries open `context` and `resource.attributes` maps; Rego reads whatever is in them. The CARING profile constrains *descriptor* vocabulary — canonical roles, planes, capabilities — not the request payload a policy may inspect. A credential-grant preflight is the same shape as the ops-warden SSH signing gate that has been in production since `FLEX-WP-0007`: bounded TTL, an actor class, and a purpose. What is *actually* missing is narrower and worth stating precisely: 1. No credential-grant policy package or protected-system manifest exists for `railiance-platform`. 2. The helper posts to `/credential-grants/authorize`, which flex-auth does not serve. **This is not the obstacle it appears to be** — the path is configurable via `--flex-auth-path` / `FLEX_AUTH_PATH`, so it can be pointed at `/v1/check` without a code change on their side. 3. The payload and response shapes genuinely do differ, and this is the one real gap. The helper posts a flat object — `grant_id`, `actor`, `actor_type`, `subject`, `purpose`, `requested_ttl`, `delivery_mode`, `audience`, `issuer`, `credential_type` — and reads `allowed` / `decision` / `status` plus optional `decision_id` and `reason`. `/v1/check` expects a `CheckRequest` (`subject` / `action` / `resource` / `context`) and returns `effect`, `reason`, `id`. Something has to translate. ## T01 - Decide where the translation lives ```task id: FLEX-WP-0012-T01 status: wait priority: medium state_hub_task_id: "a1c9ba0c-3413-4823-9f9e-ccee09cf5d74" ``` Two honest options, and the choice is a charter question rather than a technical one: **A. railiance-platform emits a `CheckRequest`.** No new flex-auth API surface. `FLEX_AUTH_PATH=/v1/check`, the helper maps its own fields into `subject`/`action`/`resource`/`context` and reads `effect`. Keeps flex-auth's decision surface singular, which is what `SCOPE.md` describes. Costs a change in a repo whose workplan is already `finished`, and pushes mapping knowledge into the consumer — the thing `FLEX-WP-0008-T01` established should be *coordinated*, not independently invented. **B. flex-auth serves a thin `/v1/credential-grants/authorize` adapter.** It translates the flat payload into a `CheckRequest`, evaluates the credential-grant package, and answers in the helper's vocabulary. No consumer change beyond setting `FLEX_AUTH_URL`. Costs a second decision surface, and risks the pattern `SCOPE.md` warns about — consumer-specific assumptions leaking into the generic model — if the adapter grows policy logic instead of staying a pure shape translation. Recommendation to argue with, not to accept by default: **B, on the strict condition that the adapter contains no authorization logic whatsoever** — it maps fields, delegates to the same engine, and returns the same decision id. The ops-warden gate is the precedent for the *policy*; the adapter is only about a consumer's wire format, and confining it to that is what keeps the generic model clean. Record the decision and its reasoning in the policy package, in the style of `FLEX-WP-0010-T02`. Done when the decision is recorded, and railiance-platform has agreed to it — this one cannot be decided unilaterally, because either option changes something they own. Progress 2026-08-23: revalidated both repositories. The helper still emits the flat payload and defaults to `/credential-grants/authorize`; flex-auth still serves only `/v1/check`. The translation choice is therefore current, not stale. Request railiance-platform agreement before implementing either wire shape. State Hub decision `1f9f257d-c9f2-4a5e-a018-8058a3f2a51a` records the two options and was automatically escalated for human resolution; coordination message `338e54d9-7313-433f-a8fe-8ad572d00365` asks the consumer to choose and place duration normalization explicitly. ## T02 - Credential-grant policy package and fixtures ```task id: FLEX-WP-0012-T02 status: done priority: medium state_hub_task_id: "7e9c4e59-e59c-4617-a87f-99142952fe78" ``` Add `examples/railiance-platform/` with a protected-system manifest, subject manifest, registry snapshot, policy package, and fixtures. Model it on `examples/ops-warden/`, which already encodes the hard part — bounded TTL against a per-resource maximum. The four denials the capability request asks for, and their shapes: | Denial | Reads | |---|---| | overbroad TTL | requested TTL vs the grant's max, as ops-warden does | | wrong actor type | `actor_type` against the grant's allowed classes (`human-operator`, `approved-agent`, `ci-runner`) | | wrong purpose | purpose required and matching the grant's permitted purposes | | disallowed delivery mode | `delivery_mode` against the grant's allowed modes | Note the TTL unit mismatch: the helper sends `requested_ttl` as a duration string (`15m`), while the ops-warden package compares a numeric `ttl_hours`. Decide where normalisation happens and make it explicit — a policy that silently mis-parses `15m` as 15 would be a fail-*open* bug in the one check most worth getting right. Prove the parse with a fixture, not an assumption. Fixtures must cover allow plus one deny per row above, and an unknown grant id. Verify against the real binary: `test-policy`, `load-registry`, and `check` against standalone request files. Done when all Rego tests and fixtures pass and default-deny is demonstrated for an unregistered grant. Completed 2026-08-23. `examples/railiance-platform/` registers all three current catalog grants and representative requester subjects. The policy uses numeric `requested_ttl_seconds`; duration parsing belongs to the selected wire translator, and strings are explicitly denied. Three embedded Rego tests and six external fixtures pass, covering allow, TTL, actor type, purpose, delivery mode, and unknown-grant default deny. Real `load-registry` and `check` commands returned `credential_grant_allowed` for the registered runtime grant and `unknown_grant` for an unregistered id. ## T03 - Implement the decided integration and prove it end to end ```task id: FLEX-WP-0012-T03 status: wait priority: medium state_hub_task_id: "3335b2b7-cf1a-411d-95b3-03c4b4c35659" ``` Implement whichever shape T01 chose. If B, the adapter must reuse the existing engine and emit the same decision id and provenance as `/v1/check` — no parallel evaluation path, no second decision log. Then close the loop against the real helper, the way `FLEX-WP-0010-T03` did rather than in isolation: run `flex-auth serve` with this registry, point `scripts/credential.py` at it via `FLEX_AUTH_URL`, and exercise an allow plus each of the four denials with `--require-flex-auth` set, so a deny actually stops issuance rather than falling through to local preauthorization. Also verify the degraded path deliberately: with `--require-flex-auth` **unset** and flex-auth unreachable, the helper is documented to continue by local preauthorization. Confirm that is still what happens, and record it — it means flex-auth is an *additional* gate here, not the deciding one, which is a materially different security posture from the tenant-engine and ops-warden gates and should not be quietly assumed to match them. **No secret material is involved anywhere in this flow.** The helper posts request metadata only. Nothing in this workplan should read, log, or persist a credential value; if a task appears to require one, that is a signal the design has drifted, not a reason to handle secrets here. Waiting on T01's wire-translation decision. The policy-side normalized request contract is implemented; either integration option must produce its numeric TTL field without changing policy semantics. Done when allow and all four denials are demonstrated against the real helper over real HTTP, `go test ./...` is green, and `gofmt`/`go vet` are clean. ## T04 - Closure and handoff ```task id: FLEX-WP-0012-T04 status: wait priority: low state_hub_task_id: "40015a87-040c-4d48-b360-fd5566dbc552" ``` Reply to capability request `893ff109` — it has been open since 2026-07-02 and deserves an answer either way, including if the answer is "declined, use `/v1/check` directly." If the work lands, ship an image (`.forgejo/workflows/image.yaml` builds it) and state plainly whether the credential-grant policy is *deployed* or only *in source* — the distinction `FLEX-WP-0010` learned the hard way. Note that the helper's flex-auth URL points at a local instance (`127.0.0.1:18090`), not the cluster Services, so deployment here may mean something different from the tenant-engine and user-engine rollouts; establish which runtime is actually meant before promising an endpoint. Update `docs/` with a credential-grant action vocabulary, matching the existing per-consumer vocabulary docs. The source-side action vocabulary is now present at `docs/railiance-platform-action-vocabulary.md`; final consumer/deployment handoff waits on T01 and T03.