diff --git a/CHANGELOG.md b/CHANGELOG.md index d2b6fce..2723826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and tag v{X.Y.Z}. The release workflow's preflight checks the section header matches the tag. --> +## [9.5.0] - 2026-10-21: Decision API approval retry fields + +### Added + +- Decision API payment approval wire fields: exact `target.server`, request `approval_id`, and response `pending_approval` and consumed `approval_id`. A hold is never an allow verdict; retry only after human approval and forward the payment only on an allow verdict for the spent approval. + ## [9.4.0] - 2026-09-14: decision provenance, the PEP capability handshake, typed policy authoring, and the simulation family deprecated The first Python SDK release carrying these entries sends the PEP capability handshake, which a platform reads from v10.4.0, and reaches `/api/v1/typed-policies`, which needs a v11.0.0 platform. Upgrade the SDK before the platform: from v11.0.0, `decide` refuses a client that does not declare redaction, including one that sends no handshake, wherever the organization has a redact override. Against an older platform this release works unchanged. diff --git a/axonflow/__init__.py b/axonflow/__init__.py index 7d355b6..c302cd0 100644 --- a/axonflow/__init__.py +++ b/axonflow/__init__.py @@ -246,7 +246,9 @@ CreateBudgetRequest, DecideRequest, DecideResponse, + DecisionApprovalRetry, DecisionCallerIdentity, + DecisionPendingApproval, DecisionProvenance, DecisionTarget, DynamicPolicyInfo, @@ -400,6 +402,8 @@ # Decision Mode PEP contract (ADR-056, #2563) "DecideRequest", "DecideResponse", + "DecisionApprovalRetry", + "DecisionPendingApproval", "DecisionProvenance", "LegacyValidatorAction", "PolicyIdentity", diff --git a/axonflow/_version.py b/axonflow/_version.py index 3236f04..3de3aaf 100644 --- a/axonflow/_version.py +++ b/axonflow/_version.py @@ -1,3 +1,3 @@ """Single source of truth for the AxonFlow SDK version.""" -__version__ = "9.4.0" +__version__ = "9.5.0" diff --git a/axonflow/types.py b/axonflow/types.py index fbde01c..890f36b 100644 --- a/axonflow/types.py +++ b/axonflow/types.py @@ -665,6 +665,7 @@ class DecisionTarget(BaseModel): type: str | None = Field(default=None, description='"llm" | "tool" | "agent".') model: str | None = Field(default=None, description="When type=llm.") provider: str | None = Field(default=None, description="When type=llm.") + server: str | None = Field(default=None, description="When type=tool.") tool: str | None = Field(default=None, description="When type=tool.") @@ -682,6 +683,27 @@ class DecideRequest(BaseModel): target: DecisionTarget = Field(default_factory=DecisionTarget) user_token: str | None = Field(default=None) context: dict[str, Any] | None = Field(default=None) + approval_id: str | None = Field( + default=None, description="Approved hold spent by an exact retry" + ) + + +class DecisionApprovalRetry(BaseModel): + """The header or body field that names a held call on retry.""" + + header: str + argument: str | None = None + body_field: str | None = None + + +class DecisionPendingApproval(BaseModel): + """A call held for a person's review; this is never an allow verdict.""" + + approval_id: str + status: str + plane: str + expires_at: datetime | None = None + retry: DecisionApprovalRetry class DecideResponse(DecisionProvenance): @@ -707,6 +729,8 @@ class DecideResponse(DecisionProvenance): policy_identities: list[PolicyIdentity] | None = Field(default=None) policy_packs: list[str] | None = Field(default=None) document_version: int | None = Field(default=None) + pending_approval: DecisionPendingApproval | None = Field(default=None) + approval_id: str | None = Field(default=None) class PlanStep(BaseModel): diff --git a/pyproject.toml b/pyproject.toml index 64ac9d2..d948b25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "axonflow" -version = "9.4.0" +version = "9.5.0" description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code" readme = "README.md" license = {text = "MIT"} diff --git a/tests/fixtures/openapi/README.md b/tests/fixtures/openapi/README.md index 3e43064..50483ab 100644 --- a/tests/fixtures/openapi/README.md +++ b/tests/fixtures/openapi/README.md @@ -7,7 +7,7 @@ It reads the schema declarations under `components.schemas` and the names of eac The four files here hold exactly that, and nothing else. `scripts/snapshot_openapi_schemas.py` derives them from the platform's `docs/api/*.yaml` at platform commit -`36e0e96b7e5c16626d394272b727b533f2b94a04`, the v11.0.0 candidate. It works on the YAML node +`ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4`, the merged platform source for the v11.2.0 train. It works on the YAML node graph, so every declaration survives in source order, including a schema declared twice in one file and a declaration with no properties. A loader that counts in-file duplicates reads the same thing here as from the source. The script drops descriptions, types, paths and the `info` block; @@ -23,10 +23,10 @@ spec. | File | sha256 of the source spec | |---|---| -| `agent-api.yaml` | `49bd1b145cd3b2d29e8cc8de240cc184d4f5d24f83b6c546894d1f0a682a032a` | -| `masfeat-api.yaml` | `2d49d6af2d5b1510b373c01fce1df2b712b32766b14d477652797746c52147e7` | -| `orchestrator-api.yaml` | `b173c9bec456e4af3aa09c63306e503caa73ca0cf61da3dfaa7b21453dcd1188` | -| `policy-api.yaml` | `090730a359bf242b6ed5c1a0d63a2831c749583bd957cc3ecc64c99ead5514b7` | +| `agent-api.yaml` | `617e6236f91dd7716baaab8e2f22ef24f1713ebfd4938cc52a60a3368e3acb1a` | +| `masfeat-api.yaml` | `eb224773c2a14fc6a84a630b652b17e010a4776da28529b13e173b002d2d4756` | +| `orchestrator-api.yaml` | `a1f96d929ff35acb1204ce5ba3c6e605b8f91d93e7fa62a2fa8a760ea8486860` | +| `policy-api.yaml` | `a1a7aff59d759033f99721a40d942e8750c6026d69f9efb0edf66016f0069ae9` | Each file's header repeats its source path, commit and digest. diff --git a/tests/fixtures/openapi/agent-api.yaml b/tests/fixtures/openapi/agent-api.yaml index 3ff2e7c..f8c0d50 100644 --- a/tests/fixtures/openapi/agent-api.yaml +++ b/tests/fixtures/openapi/agent-api.yaml @@ -1,6 +1,6 @@ # GENERATED by scripts/snapshot_openapi_schemas.py - do not edit. -# Source: docs/api/agent-api.yaml at platform commit 36e0e96b7e5c16626d394272b727b533f2b94a04 -# Source sha256: 49bd1b145cd3b2d29e8cc8de240cc184d4f5d24f83b6c546894d1f0a682a032a +# Source: docs/api/agent-api.yaml at platform commit ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4 +# Source sha256: 617e6236f91dd7716baaab8e2f22ef24f1713ebfd4938cc52a60a3368e3acb1a # Schema declarations and property names only; see tests/fixtures/openapi/README.md. components: schemas: @@ -18,6 +18,7 @@ components: "capabilities": {} "sdk_compatibility": {} "plugin_compatibility": {} + "upstream": {} "PlatformCapability": properties: "name": {} @@ -103,17 +104,6 @@ components: "enabled_override": {} "override_reason": {} "expires_at": {} - "CreateOverrideResponse": - properties: - "id": {} - "policy_id": {} - "policy_type": {} - "expires_at": {} - "ttl_seconds": {} - "requested_ttl": {} - "clamped": {} - "clamped_reason": {} - "created_at": {} "TestPatternRequest": properties: "pattern": {} @@ -165,6 +155,16 @@ components: "skip_llm": {} "context": {} "media": {} + "PolicyTestResponse": + properties: + "blocked": {} + "reason": {} + "triggered_policies": {} + "checks_performed": {} + "processing_time_ms": {} + "engine": {} + "subject_type": {} + "policy_bundle": {} "ClientResponse": properties: "success": {} @@ -173,18 +173,21 @@ components: "plan_id": {} "metadata": {} "error": {} + "code": {} "blocked": {} "block_reason": {} - "approval_request_id": {} "policy_info": {} "budget_info": {} "media_analysis": {} "engine": {} "subject_type": {} "policy_bundle": {} + "policy_packs": {} "legacy_validators": {} + "response_plane": {} "PolicyEvaluationInfo": properties: + "matched_policies": {} "policies_evaluated": {} "static_checks": {} "processing_time": {} @@ -205,7 +208,6 @@ components: "context_id": {} "approved_data": {} "policies": {} - "approval_request_id": {} "rate_limit": {} "expires_at": {} "block_reason": {} @@ -225,8 +227,17 @@ components: "pep_id": {} "audience": {} "capabilities": {} + "PendingApproval": + properties: + "approval_id": {} + "status": {} + "plane": {} + "expires_at": {} + "retry": {} + "ApprovalHoldReason": {} "DecideRequest": properties: + "approval_id": {} "stage": {} "caller_identity": {} "target": {} @@ -248,6 +259,8 @@ components: "tool": {} "DecideResponse": properties: + "pending_approval": {} + "approval_id": {} "verdict": {} "decision_id": {} "trace_id": {} @@ -426,6 +439,7 @@ components: "license_expiry": {} "MCPQueryRequest": properties: + "approval_id": {} "client_id": {} "license_key": {} "user_token": {} @@ -448,6 +462,7 @@ components: "engine": {} "subject_type": {} "policy_bundle": {} + "policy_packs": {} "legacy_validators": {} "PolicyInfo": properties: @@ -458,7 +473,6 @@ components: "processing_time_ms": {} "matched_policies": {} "exfiltration_check": {} - "dynamic_policy_info": {} "ExplainPolicy": properties: "policy_id": {} @@ -496,25 +510,11 @@ components: "tool_signature": {} "ExfiltrationCheckInfo": properties: - "exceeded": {} - "limit_type": {} "rows_returned": {} "row_limit": {} "bytes_returned": {} "byte_limit": {} - "DynamicPolicyInfo": - properties: - "policies_evaluated": {} - "matched_policies": {} - "orchestrator_reachable": {} - "processing_time_ms": {} - "DynamicPolicyMatch": - properties: - "policy_id": {} - "policy_name": {} - "policy_type": {} - "action": {} - "message": {} + "within_limits": {} "PolicyMatchInfo": properties: "policy_id": {} @@ -524,6 +524,7 @@ components: "action": {} "MCPExecuteRequest": properties: + "approval_id": {} "client_id": {} "license_key": {} "user_token": {} @@ -546,9 +547,11 @@ components: "engine": {} "subject_type": {} "policy_bundle": {} + "policy_packs": {} "legacy_validators": {} "MCPCheckInputRequest": properties: + "approval_id": {} "client_id": {} "user_token": {} "tenant_id": {} @@ -562,6 +565,8 @@ components: "content_type": {} "MCPCheckInputResponse": properties: + "pending_approval": {} + "approval_id": {} "allowed": {} "block_reason": {} "policies_evaluated": {} @@ -573,7 +578,13 @@ components: "override_existing_id": {} "redacted": {} "redacted_statement": {} + "redacted_parameters": {} "redaction_evaluated": {} + "engine": {} + "subject_type": {} + "policy_bundle": {} + "policy_packs": {} + "legacy_validators": {} "MCPCheckOutputRequest": properties: "client_id": {} @@ -599,6 +610,7 @@ components: "engine": {} "subject_type": {} "policy_bundle": {} + "policy_packs": {} "legacy_validators": {} "ConnectorRefreshResponse": properties: @@ -646,6 +658,7 @@ components: "window": {} "resets_at": {} "upgrade": {} + "code": {} "DecisionEntry": properties: "id": {} diff --git a/tests/fixtures/openapi/masfeat-api.yaml b/tests/fixtures/openapi/masfeat-api.yaml index ba5c354..c262b19 100644 --- a/tests/fixtures/openapi/masfeat-api.yaml +++ b/tests/fixtures/openapi/masfeat-api.yaml @@ -1,6 +1,6 @@ # GENERATED by scripts/snapshot_openapi_schemas.py - do not edit. -# Source: docs/api/masfeat-api.yaml at platform commit 36e0e96b7e5c16626d394272b727b533f2b94a04 -# Source sha256: 2d49d6af2d5b1510b373c01fce1df2b712b32766b14d477652797746c52147e7 +# Source: docs/api/masfeat-api.yaml at platform commit ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4 +# Source sha256: eb224773c2a14fc6a84a630b652b17e010a4776da28529b13e173b002d2d4756 # Schema declarations and property names only; see tests/fixtures/openapi/README.md. components: schemas: diff --git a/tests/fixtures/openapi/orchestrator-api.yaml b/tests/fixtures/openapi/orchestrator-api.yaml index 46f9a9a..2befa34 100644 --- a/tests/fixtures/openapi/orchestrator-api.yaml +++ b/tests/fixtures/openapi/orchestrator-api.yaml @@ -1,6 +1,6 @@ # GENERATED by scripts/snapshot_openapi_schemas.py - do not edit. -# Source: docs/api/orchestrator-api.yaml at platform commit 36e0e96b7e5c16626d394272b727b533f2b94a04 -# Source sha256: b173c9bec456e4af3aa09c63306e503caa73ca0cf61da3dfaa7b21453dcd1188 +# Source: docs/api/orchestrator-api.yaml at platform commit ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4 +# Source sha256: a1f96d929ff35acb1204ce5ba3c6e605b8f91d93e7fa62a2fa8a760ea8486860 # Schema declarations and property names only; see tests/fixtures/openapi/README.md. components: schemas: @@ -17,6 +17,23 @@ components: "separation_of_duties": {} "tier_established": {} "reserved": {} + "TemplateOmissionReport": + properties: + "omitted": {} + "modified": {} + "of": {} + "message": {} + "TypedAuthoringActivation": + properties: + "kind": {} + "root": {} + "digest": {} + "previous_digest": {} + "document_id": {} + "document_version": {} + "actor": {} + "at": {} + "reason": {} "AuthoringFinding": properties: "code": {} @@ -129,6 +146,10 @@ components: "provider_info": {} "processing_time": {} "media_analysis": {} + "engine": {} + "subject_type": {} + "policy_bundle": {} + "verdict": {} "PolicyEvaluationResult": properties: "allowed": {} @@ -142,9 +163,6 @@ components: "evaluation_error": {} "segments_resolved": {} "applied_policies_detail": {} - "override_applied": {} - "override_id": {} - "override_reason": {} "preferred_provider": {} "allowed_providers": {} "routing_reason": {} @@ -190,6 +208,10 @@ components: "metadata": {} "error": {} "policy_info": {} + "engine": {} + "subject_type": {} + "policy_bundle": {} + "verdict": {} "complexity": {} "domain": {} "parallel": {} @@ -234,6 +256,7 @@ components: "next_step_name": {} "step_result": {} "total_steps": {} + "ignored_foreign_gate_rows": {} "workflow_id": {} "RollbackPlanResponse": properties: @@ -491,6 +514,9 @@ components: "approval_url": {} "policies_evaluated": {} "policies_matched": {} + "engine": {} + "subject_type": {} + "policy_bundle": {} "cached": {} "decision_source": {} "retry_context": {} @@ -671,35 +697,6 @@ components: "approval_status": {} "paused_at_step": {} "paused_reason": {} - "MCPDynamicPolicyMatch": - properties: - "policy_id": {} - "policy_name": {} - "policy_type": {} - "action": {} - "reason": {} - "MCPPolicyEvaluationRequest": - properties: - "tenant_id": {} - "organization_id": {} - "user_id": {} - "user_role": {} - "segment_ids": {} - "connector_name": {} - "operation": {} - "statement": {} - "parameters": {} - "request_time": {} - "client_ip": {} - "metadata": {} - "MCPPolicyEvaluationResponse": - properties: - "allowed": {} - "block_reason": {} - "policies_evaluated": {} - "matched_policies": {} - "processing_time_ms": {} - "metadata": {} "RBIAISystemSummary": properties: "total_systems": {} @@ -742,6 +739,7 @@ components: properties: "success": {} "error": {} + "MultiAgentStepRefusal": {} "TripletErrorResponse": properties: "error": {} @@ -1495,6 +1493,7 @@ components: "pii_types": {} "has_extracted_text": {} "extracted_text_length": {} + "scanned": {} "estimated_cost_usd": {} "warnings": {} "structured_warnings": {} @@ -1607,7 +1606,6 @@ components: "created_at": {} "revoked_at": {} "revoked_by": {} - "CreateOverrideResponse": {} "OverrideSummary": {} "OverrideDetail": {} "DecisionListResponse": diff --git a/tests/fixtures/openapi/policy-api.yaml b/tests/fixtures/openapi/policy-api.yaml index 4717159..0b0bdda 100644 --- a/tests/fixtures/openapi/policy-api.yaml +++ b/tests/fixtures/openapi/policy-api.yaml @@ -1,6 +1,6 @@ # GENERATED by scripts/snapshot_openapi_schemas.py - do not edit. -# Source: docs/api/policy-api.yaml at platform commit 36e0e96b7e5c16626d394272b727b533f2b94a04 -# Source sha256: 090730a359bf242b6ed5c1a0d63a2831c749583bd957cc3ecc64c99ead5514b7 +# Source: docs/api/policy-api.yaml at platform commit ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4 +# Source sha256: a1a7aff59d759033f99721a40d942e8750c6026d69f9efb0edf66016f0069ae9 # Schema declarations and property names only; see tests/fixtures/openapi/README.md. components: schemas: diff --git a/tests/fixtures/wire_shape_baseline.json b/tests/fixtures/wire_shape_baseline.json index 09b4469..95a1c7c 100644 --- a/tests/fixtures/wire_shape_baseline.json +++ b/tests/fixtures/wire_shape_baseline.json @@ -118,6 +118,7 @@ "status", "tier", "timestamp", + "upstream", "version" ], "orchestrator-api.yaml": [ @@ -148,6 +149,30 @@ "timestamp" ] }, + "PendingApproval": { + "agent-api.yaml": [ + "approval_id", + "expires_at", + "plane", + "retry", + "status" + ], + "orchestrator-api.yaml": [ + "approval_status", + "created_at", + "decision", + "decision_reason", + "plan_id", + "policies_matched", + "step_id", + "step_index", + "step_input", + "step_name", + "step_type", + "workflow_id", + "workflow_name" + ] + }, "UpdatePolicyRequest": { "orchestrator-api.yaml": [ "action", @@ -173,7 +198,7 @@ ] } }, - "openapi_specs_sha": "36e0e96b7e5c16626d394272b727b533f2b94a04", + "openapi_specs_sha": "ba5201a52edd2bfbe9f1dadd736f19cae03d9cc4", "per_model_drift": { "AISystemRegistry": { "note": "deprecated-pending-removal: getaxonflow/axonflow-enterprise#3254 pin-advance batch (dataclass binding, #3262). sdk_only lists the LEGACY spellings the parser reads first-for-compatibility before falling back to the real v9.13.0 wire names (now spec-declared), here business_owner/customer_impact/model_complexity/human_reliance (real wire: owner_email/risk_rating_impact/risk_rating_complexity/risk_rating_reliance), plus deprecated fiction technical_owner (never served on 9.x). spec_only fields (data_sources/model_type/version/deployment_date/last_assessment_date/next_assessment_due/updated_by) are informational coverage gaps (PR #224). Legacy spellings burn down in the next major.", @@ -236,7 +261,9 @@ "note": "acknowledged-spec-superset: the v11.0.0-candidate spec declares `approval_request_id` on ClientResponse; the SDK does not model it yet - additive optional field, no break. Tracked in axonflow-enterprise#1704.", "sdk_only": [], "spec_only": [ - "approval_request_id" + "code", + "policy_packs", + "response_plane" ] }, "CreateStaticPolicyRequest": { @@ -268,13 +295,6 @@ "policy_version_at_decision" ] }, - "DecisionTarget": { - "note": "acknowledged-spec-superset: the v9.13.0 spec declares optional `server` on DecisionTarget (two-field (server, tool) identity contract, epic #2905 / platform #2904); the SDK does not yet model it there - additive optional, no break.", - "sdk_only": [], - "spec_only": [ - "server" - ] - }, "DynamicPolicy": { "note": "acknowledged-sdk-superset: the SDK declares optional `organization_id` on DynamicPolicy, which the v9.13.0 pin declared and the v11.0.0-candidate spec does not. An absent optional field parses as None, so nothing breaks; whether the SDK drops it is tracked in axonflow-enterprise#1704.", "sdk_only": [ @@ -282,17 +302,11 @@ ], "spec_only": [] }, - "DynamicPolicyMatch": { - "note": "deprecated-pending-removal: `reason` is marked DEPRECATED + 'Removed in v7' on its Field() docstring; the canonical wire field is `message` (already present, matches spec).", - "sdk_only": [ - "reason" - ], - "spec_only": [] - }, "ExfiltrationCheckInfo": { "note": "deprecated-pending-removal: `within_limits` is marked DEPRECATED + 'Removed in v7' on its Field() docstring; the canonical wire fields are `exceeded` + `limit_type` (already present, match spec).", "sdk_only": [ - "within_limits" + "exceeded", + "limit_type" ], "spec_only": [] }, @@ -316,13 +330,49 @@ "trigger_conditions" ] }, + "MCPCheckInputRequest": { + "sdk_only": [], + "spec_only": [ + "approval_id" + ] + }, + "MCPCheckInputResponse": { + "sdk_only": [], + "spec_only": [ + "approval_id", + "engine", + "legacy_validators", + "pending_approval", + "policy_bundle", + "policy_packs", + "redacted_parameters", + "subject_type" + ] + }, "MCPCheckOutputResponse": { "note": "acknowledged-sdk-superset: the former `redaction_evaluated` entry resolved - the v9.13.0 spec declares it (#2865/#2866). SDK still declares decision-context fields `policy_matches`/`redacted_message` (Plugin Batch 1, v7.1.0 cycle) ahead of the spec; tracked in #1745.", "sdk_only": [ "policy_matches", "redacted_message" ], - "spec_only": [] + "spec_only": [ + "policy_packs" + ] + }, + "PlanResponse": { + "sdk_only": [], + "spec_only": [ + "engine", + "policy_bundle", + "subject_type", + "verdict" + ] + }, + "PolicyEvaluationInfo": { + "sdk_only": [], + "spec_only": [ + "matched_policies" + ] }, "PolicyEvaluationResult": { "note": "acknowledged-spec-superset: the v11.0.0-candidate spec declares allowed_providers/applied_policies_detail/evaluation_error/override_applied/override_id/override_reason/preferred_provider/routing_reason/segments_resolved/severity/severity_policy_id on PolicyEvaluationResult; the SDK does not model them yet - additive server-side surface, no break. Newly visible with the v11.0.0-candidate pin; tracked in axonflow-enterprise#1704.", @@ -331,9 +381,6 @@ "allowed_providers", "applied_policies_detail", "evaluation_error", - "override_applied", - "override_id", - "override_reason", "preferred_provider", "routing_reason", "segments_resolved", @@ -373,6 +420,12 @@ ], "spec_only": [] }, + "ResumePlanResponse": { + "sdk_only": [], + "spec_only": [ + "ignored_foreign_gate_rows" + ] + }, "StaticPolicy": { "note": "acknowledged-sdk-superset: the SDK declares optional `organization_id` on StaticPolicy, which the v9.13.0 pin declared and the v11.0.0-candidate spec does not. An absent optional field parses as None, so nothing breaks; whether the SDK drops it is tracked in axonflow-enterprise#1704.", "sdk_only": [ @@ -385,7 +438,16 @@ "sdk_only": [], "spec_only": [ "approval_enqueue", - "approval_id" + "approval_id", + "engine", + "policy_bundle", + "subject_type" + ] + }, + "TemplateOmissionReport": { + "sdk_only": [], + "spec_only": [ + "modified" ] }, "UnifiedStepStatus": { @@ -419,6 +481,7 @@ "AuthZENResponse", "AuthZENResponseContext", "AuthZENSubject", + "AuthoringFinding", "Budget", "BudgetAlert", "BudgetCheckRequest", @@ -438,8 +501,7 @@ "DecisionExplanation", "DecisionTarget", "DynamicPolicy", - "DynamicPolicyInfo", - "DynamicPolicyMatch", + "EditionConstructReport", "ExecutionSnapshot", "ExecutionSummary", "ExfiltrationCheckInfo", @@ -493,9 +555,11 @@ "StaticPolicy", "StepGateRequest", "StepGateResponse", + "TemplateOmissionReport", "TimelineEntry", "TokenUsage", "ToolContext", + "TypedAuthoringDocumentRequest", "UnifiedStepStatus", "UpdatePlanRequest", "UpdatePlanResponse", diff --git a/tests/test_pep.py b/tests/test_pep.py index 5e029d9..e3a9e47 100644 --- a/tests/test_pep.py +++ b/tests/test_pep.py @@ -153,6 +153,43 @@ async def test_decide_omits_none_fields_on_wire( assert "user_token" not in body # None excluded assert "context" not in body + @pytest.mark.asyncio + async def test_payment_hold_and_approved_retry_wire( + self, client: AxonFlow, httpx_mock: HTTPXMock + ) -> None: + httpx_mock.add_response(url=DECIDE_URL, json={ + "verdict": "needs_approval", "pending_approval": { + "approval_id": "hold-1", "status": "pending", "plane": "decide", + "retry": {"header": "X-Axonflow-Approval-Id", "body_field": "approval_id"}, + }, + }) + httpx_mock.add_response(url=DECIDE_URL, json={ + "verdict": "allow", "approval_id": "hold-1", + }) + request = DecideRequest( + stage="tool", query="Retry failed disbursement", + target={ + "type": "tool", + "server": "payment-ops", + "tool": "payment-ops.retry_disbursement", + }, + ) + held = await client.decide(request) + assert held.verdict == "needs_approval" + assert held.pending_approval is not None + assert held.pending_approval.approval_id == "hold-1" + assert held.pending_approval.retry.body_field == "approval_id" + request.approval_id = held.pending_approval.approval_id + allowed = await client.decide(request) + assert allowed.verdict == "allow" + assert allowed.approval_id == "hold-1" + import json as _json + + sent = [_json.loads(item.content) for item in httpx_mock.get_requests()] + assert sent[0]["target"]["server"] == "payment-ops" + assert "approval_id" not in sent[0] + assert sent[1]["approval_id"] == "hold-1" + # --------------------------------------------------------------------------- # fulfill_request() — the fail-closed core