--- id: FLEX-WP-0012 type: workplan title: "Authorize railiance-platform credential-grant requests" domain: infotech repo: flex-auth status: proposed owner: codex topic_slug: netkingdom planning_priority: P3 planning_order: 120 related_workplans: - RAILIANCE-WP-0005 created: "2026-08-11" updated: "2026-08-11" --- # 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: todo priority: medium ``` 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. ## T02 - Credential-grant policy package and fixtures ```task id: FLEX-WP-0012-T02 status: todo priority: medium ``` 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. ## T03 - Implement the decided integration and prove it end to end ```task id: FLEX-WP-0012-T03 status: todo priority: medium ``` 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. 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: todo priority: low ``` 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.