feat: require strict signed task authorization before approval consumption
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
ebca4f5adb
commit
ff1a13dabd
5 changed files with 539 additions and 4 deletions
|
|
@ -23,7 +23,7 @@ from secrets_engine.execution_contract import ExecutionContract, require_executi
|
|||
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
|
||||
from secrets_engine.catalog import human_control_required
|
||||
from secrets_engine.decision_check import check_decision, require_supported_pdp_address
|
||||
from secrets_engine.decision_signature import load_trust
|
||||
from secrets_engine.decision_signature import load_trust, strict_json
|
||||
from secrets_engine.authorization import (
|
||||
build_action_request,
|
||||
validate_decision_envelope,
|
||||
|
|
@ -70,6 +70,8 @@ class AuthorizedAction:
|
|||
binding: ConsumeBinding
|
||||
decision_id: str
|
||||
expires_at: str
|
||||
# Detached original served artifacts for the narrow task-execution handoff.
|
||||
artifacts: str = field(default="", repr=False, compare=False)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
|
|
@ -380,6 +382,8 @@ def authorize_action(
|
|||
),
|
||||
decision_id=validated.decision_id,
|
||||
expires_at=validated.expires_at,
|
||||
artifacts=json.dumps({"request":expected_request,"claim":observation.claim,"envelope":envelope},
|
||||
sort_keys=True,separators=(",",":"),allow_nan=False),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -391,6 +395,7 @@ def consume_approval(
|
|||
binding: ConsumeBinding,
|
||||
timeout_seconds: float = 3,
|
||||
opener: Callable[..., Any] = credential_urlopen,
|
||||
strict_response: bool = False,
|
||||
) -> ConsumedApproval:
|
||||
"""POST /v1/approvals/{id}/consume. Fail closed on anything but confirmed use."""
|
||||
if not base_url or not base_url.startswith(("http://", "https://")):
|
||||
|
|
@ -438,8 +443,8 @@ def consume_approval(
|
|||
if status != 200 or len(raw) > _MAX_BODY:
|
||||
raise DecisionError(_status_message(status if status != 200 else 502))
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
payload = (strict_json if strict_response else json.loads)(raw.decode("utf-8"))
|
||||
except (UnicodeDecodeError, ValueError, RecursionError) as exc:
|
||||
raise DecisionError("approval consume returned invalid JSON") from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise DecisionError("approval consume returned invalid payload")
|
||||
|
|
@ -447,6 +452,19 @@ def consume_approval(
|
|||
raise DecisionError("approval consumption was not confirmed")
|
||||
if payload.get("request_digest") != binding.request_digest:
|
||||
raise DecisionError("approval consume digest does not match the request")
|
||||
if strict_response:
|
||||
from datetime import datetime
|
||||
if (payload.get("approval_id") != approval_id
|
||||
or payload.get("decision_id") != binding.decision_id
|
||||
or payload.get("idempotent") is not False
|
||||
or any(key in payload for key in ("effect","allow","deny","decision"))):
|
||||
raise DecisionError("strict task consumption correspondence refused")
|
||||
try:
|
||||
stamp = datetime.fromisoformat(payload["consumed_at"].replace("Z","+00:00"))
|
||||
if stamp.tzinfo is None or stamp.utcoffset() is None:
|
||||
raise ValueError("timezone required")
|
||||
except (KeyError,TypeError,AttributeError,ValueError):
|
||||
raise DecisionError("strict task consumption timestamp refused") from None
|
||||
decision_id = payload.get("decision_id")
|
||||
if decision_id is not None and (
|
||||
not isinstance(decision_id, str) or not decision_id
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue