Adopt canonical flex-auth credential checks
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02e56-e4ad-71a2-b3e2-b6193e0d8093
This commit is contained in:
parent
8ae77ca006
commit
c9d02147d3
8 changed files with 338 additions and 50 deletions
|
|
@ -27,11 +27,13 @@
|
|||
| workplan | RAILIANCE-WP-0025 | finished | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md |
|
||||
| workplan | RAILIANCE-WP-0026 | finished | — | workplans/RAILIANCE-WP-0026-attended-login-output-containment.md |
|
||||
| workplan | RAILIANCE-WP-0027 | blocked | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md |
|
||||
| workplan | RAILIANCE-WP-0028 | finished | — | workplans/RAILIANCE-WP-0028-flex-auth-credential-grant-integration.md |
|
||||
| workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| workplan | RPF-WP-0020 | finished | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||
| task | ADHOC-2026-08-23-T01 | done | — | workplans/ADHOC-2026-08-23.md |
|
||||
| task | ADHOC-2026-08-23-T02 | done | — | workplans/ADHOC-2026-08-23.md |
|
||||
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
| task | RAILIANCE-WP-0005-T03 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
|
|
@ -116,6 +118,9 @@
|
|||
| task | RAILIANCE-WP-0027-T01 | done | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md |
|
||||
| task | RAILIANCE-WP-0027-T02 | done | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md |
|
||||
| task | RAILIANCE-WP-0027-T03 | wait | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md |
|
||||
| task | RAILIANCE-WP-0028-T01 | done | — | workplans/RAILIANCE-WP-0028-flex-auth-credential-grant-integration.md |
|
||||
| task | RAILIANCE-WP-0028-T02 | done | — | workplans/RAILIANCE-WP-0028-flex-auth-credential-grant-integration.md |
|
||||
| task | RAILIANCE-WP-0028-T03 | done | — | workplans/RAILIANCE-WP-0028-flex-auth-credential-grant-integration.md |
|
||||
| task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
|
|
|
|||
|
|
@ -213,12 +213,16 @@ The helper performs local catalog checks before any issuance:
|
|||
- actor type must be allowed by the grant.
|
||||
|
||||
Optional flex-auth preflight is enabled with `--flex-auth-url` or `FLEX_AUTH_URL`.
|
||||
The helper posts non-secret request metadata to
|
||||
`/credential-grants/authorize` by default and accepts allow/deny responses using
|
||||
`allowed`, `decision`, or `status` fields plus optional `decision_id` and
|
||||
`reason`. Use `--require-flex-auth` when local preauthorization is not
|
||||
acceptable. Use `--decision-id` to carry an already-approved external decision
|
||||
without calling flex-auth again.
|
||||
The helper posts non-secret request metadata to canonical `POST /v1/check` by
|
||||
default. It maps the grant to a `credential-grant:<grant-id>` resource, action
|
||||
`issue`, and the bound actor subject. The helper parses `requested_ttl` locally
|
||||
and sends integer `context.requested_ttl_seconds`; flex-auth policy never has to
|
||||
infer duration units. Only a DecisionEnvelope with `effect: allow` succeeds;
|
||||
the helper also requires its decision id, evaluator provenance, and matching
|
||||
subject/resource binding. Every other or malformed response fails closed. Use
|
||||
`--require-flex-auth` when local preauthorization is not acceptable. Use
|
||||
`--decision-id` to carry an already-approved external decision without calling
|
||||
flex-auth again.
|
||||
|
||||
## State Hub Metadata
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ UNSAFE_VERBOSE_VALUES = {"debug", "trace"}
|
|||
DEFAULT_ACTOR_TYPE = "approved-agent"
|
||||
DEFAULT_ACTOR = f"codex:{os.environ.get('USER', 'unknown')}"
|
||||
DEFAULT_SUBJECT = "agent:codex/railiance-platform"
|
||||
FLEX_AUTH_SUBJECT_TYPES = {
|
||||
"human-operator": "Human",
|
||||
"approved-agent": "Agent",
|
||||
"ci-runner": "Automation",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -207,7 +212,7 @@ def post_json(
|
|||
return data
|
||||
|
||||
|
||||
def request_metadata(
|
||||
def flex_auth_check_request(
|
||||
*,
|
||||
grant: dict[str, Any],
|
||||
ttl: str,
|
||||
|
|
@ -217,17 +222,26 @@ def request_metadata(
|
|||
actor_type: str,
|
||||
subject: str,
|
||||
) -> dict[str, Any]:
|
||||
subject_type = FLEX_AUTH_SUBJECT_TYPES.get(actor_type)
|
||||
if subject_type is None:
|
||||
fail(f"actor type {actor_type!r} has no flex-auth subject type mapping")
|
||||
return {
|
||||
"grant_id": grant["id"],
|
||||
"actor": actor,
|
||||
"actor_type": actor_type,
|
||||
"subject": subject,
|
||||
"purpose": purpose,
|
||||
"requested_ttl": ttl,
|
||||
"delivery_mode": delivery,
|
||||
"audience": grant.get("audience"),
|
||||
"issuer": grant.get("issuer"),
|
||||
"credential_type": grant.get("credential_type"),
|
||||
"tenant": "tenant:platform",
|
||||
"subject": {"id": subject, "type": subject_type},
|
||||
"action": "issue",
|
||||
"resource": {
|
||||
"id": f"credential-grant:{grant['id']}",
|
||||
"type": "credential-grant",
|
||||
"system": "railiance-platform",
|
||||
},
|
||||
"context": {
|
||||
"actor": actor,
|
||||
"actor_type": actor_type,
|
||||
"bound_subject": subject,
|
||||
"purpose": purpose,
|
||||
"delivery_mode": delivery,
|
||||
"requested_ttl_seconds": ttl_seconds(ttl),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -264,7 +278,7 @@ def authorize_request(
|
|||
)
|
||||
|
||||
endpoint = join_url(args.flex_auth_url, args.flex_auth_path)
|
||||
payload = request_metadata(
|
||||
payload = flex_auth_check_request(
|
||||
grant=grant,
|
||||
ttl=ttl,
|
||||
purpose=purpose,
|
||||
|
|
@ -288,30 +302,36 @@ def authorize_request(
|
|||
"flex-auth unavailable; local preauthorization used",
|
||||
)
|
||||
|
||||
allowed_value = response.get("allowed")
|
||||
decision_value = str(
|
||||
response.get("decision") or response.get("status") or ""
|
||||
).lower()
|
||||
allowed = allowed_value is True or decision_value in {
|
||||
"allow",
|
||||
"allowed",
|
||||
"approved",
|
||||
"pass",
|
||||
}
|
||||
denied = allowed_value is False or decision_value in {
|
||||
"deny",
|
||||
"denied",
|
||||
"rejected",
|
||||
"fail",
|
||||
}
|
||||
decision_id = response.get("decision_id") or response.get("id")
|
||||
reason = response.get("reason") or response.get("message")
|
||||
if denied or not allowed:
|
||||
effect = response.get("effect")
|
||||
decision_id = response.get("id")
|
||||
reason = response.get("reason")
|
||||
if effect != "allow":
|
||||
fail(f"flex-auth denied credential request: {reason or 'no reason supplied'}")
|
||||
if not isinstance(decision_id, str) or not decision_id:
|
||||
fail("flex-auth returned an allow effect without a decision id")
|
||||
response_subject = response.get("subject")
|
||||
if (
|
||||
not isinstance(response_subject, dict)
|
||||
or response_subject.get("id") != payload["subject"]["id"]
|
||||
):
|
||||
fail("flex-auth allow decision subject does not match the request")
|
||||
response_resource = response.get("resource")
|
||||
if not isinstance(response_resource, dict) or any(
|
||||
response_resource.get(field) != payload["resource"][field]
|
||||
for field in ("id", "type", "system")
|
||||
):
|
||||
fail("flex-auth allow decision resource does not match the request")
|
||||
provenance = response.get("provenance")
|
||||
if (
|
||||
not isinstance(provenance, dict)
|
||||
or not provenance.get("evaluator")
|
||||
or not provenance.get("mode")
|
||||
):
|
||||
fail("flex-auth allow decision has no evaluator provenance")
|
||||
return AuthorizationResult(
|
||||
True,
|
||||
"flex-auth",
|
||||
str(decision_id) if decision_id else None,
|
||||
decision_id,
|
||||
str(reason) if reason else None,
|
||||
)
|
||||
|
||||
|
|
@ -1096,7 +1116,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
parser.add_argument("--flex-auth-url", default=os.environ.get("FLEX_AUTH_URL"))
|
||||
parser.add_argument(
|
||||
"--flex-auth-path",
|
||||
default=os.environ.get("FLEX_AUTH_PATH", "/credential-grants/authorize"),
|
||||
default=os.environ.get("FLEX_AUTH_PATH", "/v1/check"),
|
||||
)
|
||||
parser.add_argument("--require-flex-auth", action="store_true")
|
||||
parser.add_argument(
|
||||
|
|
|
|||
|
|
@ -121,7 +121,14 @@ def verify_adapter(
|
|||
}
|
||||
|
||||
|
||||
def receipt_base(contract: dict[str, Any], reviewer: str, decision: str) -> dict[str, Any]:
|
||||
def receipt_base(
|
||||
contract: dict[str, Any],
|
||||
reviewer: str,
|
||||
decision: str,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> dict[str, Any]:
|
||||
created_at = now or datetime.now(UTC)
|
||||
return {
|
||||
"interface": BROKER_INTERFACE,
|
||||
"version": 1,
|
||||
|
|
@ -129,7 +136,7 @@ def receipt_base(contract: dict[str, Any], reviewer: str, decision: str) -> dict
|
|||
"owner": BROKER_OWNER,
|
||||
"reviewer": validate_reviewer(reviewer),
|
||||
"decision": decision,
|
||||
"created_at": datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
||||
"created_at": created_at.replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
||||
"engagement_id": contract["engagement_id"],
|
||||
"target_id": contract["target"]["id"],
|
||||
"projection_contract_digest": contract_digest(contract),
|
||||
|
|
@ -142,22 +149,32 @@ def receipt_base(contract: dict[str, Any], reviewer: str, decision: str) -> dict
|
|||
|
||||
|
||||
def build_approval(
|
||||
contract: dict[str, Any], reviewer: str, verification: dict[str, Any]
|
||||
contract: dict[str, Any],
|
||||
reviewer: str,
|
||||
verification: dict[str, Any],
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if verification.get("passed") is not True or verification.get("secret_values_observed") is not False:
|
||||
raise ReadinessError("consumer adapter verification did not pass")
|
||||
receipt = {
|
||||
**receipt_base(contract, reviewer, "approve"),
|
||||
**receipt_base(contract, reviewer, "approve", now=now),
|
||||
"adapter": verification["adapter"],
|
||||
"cleanup_request_supported": True,
|
||||
}
|
||||
validate_broker_receipt(receipt, contract)
|
||||
validate_broker_receipt(receipt, contract, now=now)
|
||||
return receipt
|
||||
|
||||
|
||||
def build_change_request(contract: dict[str, Any], reviewer: str, note: str) -> dict[str, Any]:
|
||||
def build_change_request(
|
||||
contract: dict[str, Any],
|
||||
reviewer: str,
|
||||
note: str,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
**receipt_base(contract, reviewer, "request-changes"),
|
||||
**receipt_base(contract, reviewer, "request-changes", now=now),
|
||||
"note": validate_note(note),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import tempfile
|
|||
import unittest
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
|
|
@ -50,6 +51,139 @@ def sample_grant() -> dict:
|
|||
|
||||
|
||||
class CredentialHelperTests(unittest.TestCase):
|
||||
def test_flex_auth_request_uses_canonical_contract_and_numeric_ttl(self) -> None:
|
||||
payload = credential.flex_auth_check_request(
|
||||
grant=sample_grant(),
|
||||
ttl="15m",
|
||||
purpose="flex-auth-openbao-smoke",
|
||||
delivery="exec-env",
|
||||
actor="codex:operator",
|
||||
actor_type="approved-agent",
|
||||
subject="agent:codex/railiance-platform",
|
||||
)
|
||||
self.assertEqual(payload["tenant"], "tenant:platform")
|
||||
self.assertEqual(
|
||||
payload["subject"],
|
||||
{"id": "agent:codex/railiance-platform", "type": "Agent"},
|
||||
)
|
||||
self.assertEqual(payload["action"], "issue")
|
||||
self.assertEqual(
|
||||
payload["resource"],
|
||||
{
|
||||
"id": "credential-grant:ops-warden/warden-sign",
|
||||
"type": "credential-grant",
|
||||
"system": "railiance-platform",
|
||||
},
|
||||
)
|
||||
self.assertEqual(payload["context"]["requested_ttl_seconds"], 900)
|
||||
self.assertEqual(
|
||||
payload["context"]["bound_subject"],
|
||||
"agent:codex/railiance-platform",
|
||||
)
|
||||
|
||||
def test_authorize_request_reads_decision_envelope(self) -> None:
|
||||
args = Namespace(
|
||||
decision_id=None,
|
||||
dry_run=False,
|
||||
flex_auth_url="http://127.0.0.1:19099",
|
||||
flex_auth_path="/v1/check",
|
||||
require_flex_auth=True,
|
||||
actor="codex:operator",
|
||||
actor_type="approved-agent",
|
||||
subject="agent:codex/railiance-platform",
|
||||
http_timeout=1.0,
|
||||
)
|
||||
response = {
|
||||
"id": "decision:credential-grant-allow",
|
||||
"effect": "allow",
|
||||
"reason": "credential_grant_allowed",
|
||||
"subject": {
|
||||
"id": "agent:codex/railiance-platform",
|
||||
"type": "Agent",
|
||||
},
|
||||
"resource": {
|
||||
"id": "credential-grant:ops-warden/warden-sign",
|
||||
"type": "credential-grant",
|
||||
"system": "railiance-platform",
|
||||
},
|
||||
"provenance": {"evaluator": "standalone", "mode": "embedded"},
|
||||
}
|
||||
with mock.patch.object(credential, "post_json", return_value=response) as post:
|
||||
result = credential.authorize_request(
|
||||
args=args,
|
||||
grant=sample_grant(),
|
||||
ttl="15m",
|
||||
purpose="flex-auth-openbao-smoke",
|
||||
delivery="exec-env",
|
||||
)
|
||||
self.assertEqual(result.mode, "flex-auth")
|
||||
self.assertEqual(result.decision_id, "decision:credential-grant-allow")
|
||||
endpoint, payload = post.call_args.args
|
||||
self.assertEqual(endpoint, "http://127.0.0.1:19099/v1/check")
|
||||
self.assertEqual(payload["context"]["requested_ttl_seconds"], 900)
|
||||
|
||||
def test_authorize_request_fails_closed_on_non_allow_effect(self) -> None:
|
||||
args = Namespace(
|
||||
decision_id=None,
|
||||
dry_run=False,
|
||||
flex_auth_url="http://127.0.0.1:19099",
|
||||
flex_auth_path="/v1/check",
|
||||
require_flex_auth=True,
|
||||
actor="codex:operator",
|
||||
actor_type="approved-agent",
|
||||
subject="agent:codex/railiance-platform",
|
||||
http_timeout=1.0,
|
||||
)
|
||||
with mock.patch.object(
|
||||
credential,
|
||||
"post_json",
|
||||
return_value={
|
||||
"id": "decision:deny",
|
||||
"effect": "deny",
|
||||
"reason": "ttl_out_of_bounds",
|
||||
},
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "ttl_out_of_bounds"):
|
||||
credential.authorize_request(
|
||||
args=args,
|
||||
grant=sample_grant(),
|
||||
ttl="15m",
|
||||
purpose="flex-auth-openbao-smoke",
|
||||
delivery="exec-env",
|
||||
)
|
||||
|
||||
def test_authorize_request_rejects_unbound_allow_response(self) -> None:
|
||||
args = Namespace(
|
||||
decision_id=None,
|
||||
dry_run=False,
|
||||
flex_auth_url="http://127.0.0.1:19099",
|
||||
flex_auth_path="/v1/check",
|
||||
require_flex_auth=True,
|
||||
actor="codex:operator",
|
||||
actor_type="approved-agent",
|
||||
subject="agent:codex/railiance-platform",
|
||||
http_timeout=1.0,
|
||||
)
|
||||
with mock.patch.object(
|
||||
credential,
|
||||
"post_json",
|
||||
return_value={"id": "decision:unbound", "effect": "allow"},
|
||||
):
|
||||
with self.assertRaisesRegex(SystemExit, "subject does not match"):
|
||||
credential.authorize_request(
|
||||
args=args,
|
||||
grant=sample_grant(),
|
||||
ttl="15m",
|
||||
purpose="flex-auth-openbao-smoke",
|
||||
delivery="exec-env",
|
||||
)
|
||||
|
||||
def test_flex_auth_default_path_is_canonical_check(self) -> None:
|
||||
args = credential.build_parser().parse_args(
|
||||
["request", "--purpose", "flex-auth-openbao-smoke"]
|
||||
)
|
||||
self.assertEqual(args.flex_auth_path, "/v1/check")
|
||||
|
||||
def test_database_credential_response_is_parsed_without_emission(self) -> None:
|
||||
lease_id, username, password = credential.parse_database_credential(
|
||||
'{"lease_id":"database/creds/runtime/lease-1","data":{"username":"leased-user","password":"leased-secret"}}'
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import stat
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
|
@ -28,6 +29,12 @@ SPEC.loader.exec_module(module)
|
|||
|
||||
|
||||
class BrokerReadinessTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def engagement_time(contract: dict) -> datetime:
|
||||
return datetime.fromisoformat(
|
||||
contract["window"]["starts_at"].replace("Z", "+00:00")
|
||||
)
|
||||
|
||||
def test_show_is_direct_and_authorizes_no_live_mutation(self) -> None:
|
||||
result = module.show(projection_contract())
|
||||
self.assertEqual("whitehat-security", result["owner"])
|
||||
|
|
@ -78,13 +85,21 @@ class BrokerReadinessTests(unittest.TestCase):
|
|||
},
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
receipt = module.build_approval(contract, "whitehat-owner", verification)
|
||||
now = self.engagement_time(contract)
|
||||
receipt = module.build_approval(
|
||||
contract, "whitehat-owner", verification, now=now
|
||||
)
|
||||
self.assertEqual("approve", receipt["decision"])
|
||||
self.assertTrue(receipt["cleanup_request_supported"])
|
||||
change = module.build_change_request(contract, "whitehat-owner", "Bind cleanup acknowledgement.")
|
||||
change = module.build_change_request(
|
||||
contract,
|
||||
"whitehat-owner",
|
||||
"Bind cleanup acknowledgement.",
|
||||
now=now,
|
||||
)
|
||||
self.assertEqual("request-changes", change["decision"])
|
||||
with self.assertRaises(module.ReadinessError):
|
||||
module.build_change_request(contract, "whitehat-owner", "")
|
||||
module.build_change_request(contract, "whitehat-owner", "", now=now)
|
||||
|
||||
def test_receipt_export_is_raw_canonical_document_with_private_mode(self) -> None:
|
||||
contract = projection_contract()
|
||||
|
|
@ -99,7 +114,12 @@ class BrokerReadinessTests(unittest.TestCase):
|
|||
},
|
||||
"secret_values_observed": False,
|
||||
}
|
||||
receipt = module.build_approval(contract, "whitehat-owner", verification)
|
||||
receipt = module.build_approval(
|
||||
contract,
|
||||
"whitehat-owner",
|
||||
verification,
|
||||
now=self.engagement_time(contract),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "nested" / "broker.json"
|
||||
module.write_receipt(path, receipt)
|
||||
|
|
|
|||
|
|
@ -28,3 +28,20 @@ the new entries satisfy the current-posture evidence contract directly.
|
|||
|
||||
Verification runs the canonical NetKingdom tenancy-posture validator against
|
||||
this repository's declaration and checks the repository diff.
|
||||
|
||||
## Stabilize expired-engagement broker readiness tests
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-08-23-T02
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Full test discovery exposed two WP0025 broker-readiness tests that built a
|
||||
receipt with wall-clock time for a terminal prior-day engagement. Add an
|
||||
optional explicit clock to the receipt builders and pin the unit fixtures to
|
||||
the contract window so the production default stays current UTC while offline
|
||||
tests remain deterministic.
|
||||
|
||||
The complete 146-test offline suite now passes on dates after the terminal
|
||||
engagement window.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
id: RAILIANCE-WP-0028
|
||||
type: workplan
|
||||
title: "Adopt canonical flex-auth credential-grant checks"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-23"
|
||||
updated: "2026-08-23"
|
||||
related:
|
||||
- FLEX-WP-0012
|
||||
origin: routed
|
||||
origin_ref: "State Hub decision 1f9f257d-c9f2-4a5e-a018-8058a3f2a51a"
|
||||
---
|
||||
|
||||
# RAILIANCE-WP-0028 — canonical flex-auth credential-grant checks
|
||||
|
||||
## Goal
|
||||
|
||||
Adopt flex-auth's single canonical `POST /v1/check` decision surface for
|
||||
credential-grant preflight without exposing credential values or adding a
|
||||
consumer-specific flex-auth endpoint.
|
||||
|
||||
## T01 — Resolve translation ownership
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0028-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
The operator explicitly approved Option A in State Hub decision
|
||||
`1f9f257d-c9f2-4a5e-a018-8058a3f2a51a`: railiance-platform maps its grant
|
||||
metadata to `CheckRequest` and reads `DecisionEnvelope`. Duration parsing and
|
||||
normalization belong here, before the flex-auth policy boundary.
|
||||
|
||||
## T02 — Implement the canonical request and response
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0028-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Change the helper default to `/v1/check`, emit the coordinated
|
||||
`tenant:platform` / `credential-grant:<id>` / `issue` request vocabulary, send
|
||||
`requested_ttl_seconds` as an integer, and accept only `effect: allow`.
|
||||
|
||||
Completed in source. Actor classes map to canonical subject types (`Human`,
|
||||
`Agent`, or `Automation`); the bound subject and non-secret actor metadata are
|
||||
carried in context. Missing, deny, redact, audit-only, and not-applicable
|
||||
effects all fail closed. An allow also requires a non-empty decision id,
|
||||
evaluator provenance, and subject/resource binding back to the request.
|
||||
|
||||
## T03 — Verify and route the contract
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0028-T03
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Exercise focused allow and deny tests, validate the complete repository suite,
|
||||
and route the adopted mapping to flex-auth without requesting live credentials
|
||||
or a production mutation.
|
||||
|
||||
Completed with 70 focused credential tests, all credential-helper dry-runs,
|
||||
credential-catalog validation, and the complete 146-test offline suite passing.
|
||||
No flex-auth production endpoint was called and no credential was issued.
|
||||
Loading…
Add table
Add a link
Reference in a new issue