Adopt canonical flex-auth credential checks
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02e56-e4ad-71a2-b3e2-b6193e0d8093
This commit is contained in:
codex 2026-08-23 14:03:40 +02:00
parent 8ae77ca006
commit c9d02147d3
8 changed files with 338 additions and 50 deletions

View file

@ -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(

View file

@ -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),
}