flex-auth/workplans/FLEX-WP-0012-credential-grant-authorization-surface.md
tegwick acbaa4a7c9
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 37s
feat(policy): add credential grant authorization package
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e47-6aac-7ee1-914d-0584c75d3c81
2026-08-23 13:59:03 +02:00

11 KiB

id type title domain repo status owner topic_slug planning_priority planning_order related_workplans created updated state_hub_workstream_id
FLEX-WP-0012 workplan Authorize railiance-platform credential-grant requests infotech flex-auth active codex netkingdom P3 120
RAILIANCE-WP-0005
2026-08-11 2026-08-23 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

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

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

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

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.