Plan credential-grant authorization for railiance-platform (FLEX-WP-0012)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Opens the State Hub capability request 893ff109, unanswered since
2026-07-02. Drafted as proposed/P3 rather than as the blocker it was
filed as, because two of its premises no longer hold:

- It is not blocking anyone. railiance-platform re-scoped and closed
  RAILIANCE-WP-0005-T07 the same day it filed the request; WP-0005 is
  finished and their grant catalog already enforces TTL, actor-type,
  purpose and delivery-mode bounds locally. This buys defence in depth
  and central decision records, not unblocking.
- The stated technical blocker is wrong. The request says the CARING
  /v1/check schema cannot express TTL, purpose or delivery mode; the
  ops-warden package in this repo already denies ttl_out_of_bounds by
  reading input.context.ttl_hours against resource.attributes. CARING
  constrains descriptor vocabulary, not the request payload.

The real gap is narrower: no credential-grant policy package exists, and
the helper's flat payload/response shape differs from CheckRequest. Its
endpoint path is configurable, so no bespoke endpoint is strictly
required. T01 makes the adapter-vs-consumer-mapping call an explicit,
jointly-agreed decision rather than a default.

Flags two things worth knowing before anyone implements: the helper sends
requested_ttl as a duration string while the ops-warden precedent compares
numeric hours (mis-parsing it would fail open on the check that matters
most), and without --require-flex-auth the helper continues by local
preauthorization when flex-auth is unreachable -- a different posture from
the tenant-engine and ops-warden gates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-11 13:38:41 +02:00
parent 3dc6223cc2
commit 693136edbc

View file

@ -0,0 +1,213 @@
---
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.