Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- Governed trusted-composition MCP compatibility and real STDIO checks prove the same protocol-error boundary for action-level executor failure: safe `ActionExecutionFailureEvidence` remains in error `data`, the controlled failing resource shows zero observed fixture mutation without rewriting `external_side_effect_state=unknown`, and a subsequent valid mutation succeeds.
- The MCP protocol guard removes this failure class from the normal model-correctable Tool-result channel; it does not prove that every host will avoid programmatic retry and does not provide idempotency, rollback, compensation, or distributed transaction semantics.
- Agno failure regression coverage proves a mutable executor is attempted exactly once and callers receive the original governed failure evidence with exact context/action/authorization, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`; raw executor text remains only in the local exception cause.
- Cross-framework governed-failure conformance now proves direct runtime, LangGraph, CrewAI, LlamaIndex, and Agno preserve baseline-equivalent `ActionExecutionFailureEvidence` for an authorized executor failure, with exactly one attempt and raw executor text confined to the local exception cause.

## 1.3.0 - 2026-09-05

Expand Down
8 changes: 6 additions & 2 deletions docs/security/GOVERNED_AGENT_ACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,15 +483,19 @@ The controlled matrix covers:
| caller mismatch | fail-closed `deny` | none |
| identity-source mismatch | fail-closed `deny` | none |
| resource escalation | fail-closed `deny` | none |
| authorized executor failure | `GovernedActionExecutionError` with baseline-equivalent `ActionExecutionFailureEvidence` | external side-effect state remains `unknown`; exactly one attempt |

For every framework, the suite compares:

- the complete `ActionExecutionEvidence` object;
- the complete `ActionExecutionEvidence` object for successful and pre-executor paths;
- the complete `ActionExecutionFailureEvidence` object for the authorized executor-failure path;
- observable in-memory state;
- successful execution count.
- successful execution or executor-attempt count, as applicable.

The identity-source mismatch scenario uses the same allowed caller and action scope but changes only the trusted context from `trusted_composition` to `api_key`. Because no API-key-specific rule exists in that fixture, the direct runtime and all four framework adapters must return `deny / no_matching_rule` with zero side effects.

The executor-failure scenario uses the exact allowed action and a deterministic executor that raises only after invocation. The direct application runtime establishes the failure baseline, and every framework must surface `GovernedActionExecutionError` with equal structured evidence, exactly one executor attempt, and the raw executor message absent from both structured evidence and the governed error text. The raw exception remains only as the local Python cause, while `external_side_effect_state` remains `unknown`.

This is **provider-free cross-framework conformance evidence**. It does not prove production security, provider behavior, remote identity propagation, or statistical performance.

Relevant test:
Expand Down
75 changes: 74 additions & 1 deletion tests/integration/test_governed_action_framework_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
)
from agentic_lab.application.action_runtime import (
ActionExecutionEvidence,
ActionExecutor,
GovernedActionExecutionError,
GovernedActionRuntime,
)

Expand All @@ -39,6 +41,7 @@
_APPROVED_AT = datetime(2026, 9, 5, 20, 0, tzinfo=UTC)
_NOW = _APPROVED_AT + timedelta(minutes=5)
_EXPIRES_AT = _APPROVED_AT + timedelta(minutes=15)
_RAW_EXECUTOR_ERROR = "synthetic cross-framework executor failure"

_RULES: dict[ActionAuthorizationRuleKey, AuthorizationOutcome] = {
(
Expand Down Expand Up @@ -82,6 +85,18 @@ class _Scenario:
revoke_approval: bool = False


class _FailingExecutor:
"""Count one mutable invocation before raising a deterministic failure."""

def __init__(self) -> None:
self.calls = 0

def execute(self, proposed_action: ProposedAction) -> None:
"""Fail after the application has crossed the executor boundary."""
self.calls += 1
raise RuntimeError(_RAW_EXECUTOR_ERROR)


def _action(
*,
resource: str = _FINDING_RESOURCE,
Expand Down Expand Up @@ -210,7 +225,7 @@ def now(self) -> datetime:


def _runtime(
executor: InMemoryFindingAcknowledgementExecutor,
executor: ActionExecutor,
scenario: _Scenario,
) -> GovernedActionRuntime:
approvals: tuple[HumanApprovalEvidence, ...] = ()
Expand Down Expand Up @@ -312,6 +327,30 @@ def _assert_expected_behavior(
assert executor.is_acknowledged(_FINDING_RESOURCE) is scenario.expected_execution


def _assert_failure_behavior(
error: GovernedActionExecutionError,
executor: _FailingExecutor,
scenario: _Scenario,
) -> None:
evidence = error.evidence

assert executor.calls == 1
assert evidence.proposed_action == scenario.proposed_action
assert evidence.context == scenario.context
assert evidence.authorization.outcome == "allow"
assert evidence.authorization.reason == "explicit_allow"
assert evidence.approval_status == "not_applicable"
assert evidence.human_approval is None
assert evidence.approver_authorization is None
assert evidence.execution_attempted is True
assert evidence.failure_reason == "executor_error"
assert evidence.external_side_effect_state == "unknown"
assert _RAW_EXECUTOR_ERROR not in evidence.model_dump_json()
assert _RAW_EXECUTOR_ERROR not in str(error)
assert isinstance(error.__cause__, RuntimeError)
assert str(error.__cause__) == _RAW_EXECUTOR_ERROR


@pytest.mark.parametrize(
("framework_name", "runner"),
_FRAMEWORK_RUNNERS,
Expand Down Expand Up @@ -341,3 +380,37 @@ def test_framework_governed_actions_match_application_baseline(
assert framework_evidence == baseline_evidence, (
f"{framework_name} diverged from the application baseline for {scenario.name}"
)


@pytest.mark.parametrize(
("framework_name", "runner"),
_FRAMEWORK_RUNNERS,
ids=[name for name, _ in _FRAMEWORK_RUNNERS],
)
def test_framework_executor_failures_match_application_baseline(
framework_name: str,
runner: FrameworkRunner,
) -> None:
"""Require every framework to preserve application-owned executor failure provenance."""
scenario = _SCENARIOS[0]

baseline_executor = _FailingExecutor()
with pytest.raises(GovernedActionExecutionError) as baseline_caught:
_runtime(baseline_executor, scenario).execute(
scenario.proposed_action,
scenario.context,
)
_assert_failure_behavior(baseline_caught.value, baseline_executor, scenario)

framework_executor = _FailingExecutor()
with pytest.raises(GovernedActionExecutionError) as framework_caught:
runner(
_runtime(framework_executor, scenario),
scenario.context,
scenario.proposed_action,
)
_assert_failure_behavior(framework_caught.value, framework_executor, scenario)

assert framework_caught.value.evidence == baseline_caught.value.evidence, (
f"{framework_name} diverged from the application failure baseline"
)