From 56e30315e1fedf26d22b2ee7ff48feb5d9a8dee1 Mon Sep 17 00:00:00 2001 From: Bruno Vicco Date: Sun, 6 Sep 2026 01:32:02 -0300 Subject: [PATCH 1/6] test: require Agno governed failure evidence preservation --- .../agno/test_agno_action_workflow.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/unit/adapters/agno/test_agno_action_workflow.py b/tests/unit/adapters/agno/test_agno_action_workflow.py index a6a8a96..21ef26f 100644 --- a/tests/unit/adapters/agno/test_agno_action_workflow.py +++ b/tests/unit/adapters/agno/test_agno_action_workflow.py @@ -14,10 +14,14 @@ ProposedAction, StaticActionAuthorizationPolicy, ) -from agentic_lab.application.action_runtime import GovernedActionRuntime +from agentic_lab.application.action_runtime import ( + GovernedActionExecutionError, + GovernedActionRuntime, +) _FINDING_RESOURCE = "finding:demo-001" _ALLOWED_CALLER = "remediation-agent" +_RAW_EXECUTOR_ERROR = f"synthetic failure for {_FINDING_RESOURCE}" class _FailingExecutor: @@ -147,18 +151,33 @@ def test_agno_resource_escalation_fails_closed_before_mutation() -> None: assert executor.execution_count == 0 -def test_agno_mutable_step_does_not_retry_executor_failure() -> None: - """Prevent framework retry policy from repeating a failed mutable execution.""" +def test_agno_mutable_step_preserves_governed_failure_without_retry() -> None: + """Preserve typed failure provenance while preventing repeated mutable execution.""" executor = _FailingExecutor() + context = ActionContext(caller_id=_ALLOWED_CALLER) + proposed_action = _action() runtime = AgnoGovernedActionRuntime( runtime=GovernedActionRuntime( authorizer=_policy(), executor=executor, ), - context=ActionContext(caller_id=_ALLOWED_CALLER), + context=context, ) - with pytest.raises(RuntimeError): - runtime.run(_action()) + with pytest.raises(GovernedActionExecutionError) as caught: + runtime.run(proposed_action) + error = caught.value assert executor.calls == 1 + assert error.evidence.proposed_action == proposed_action + assert error.evidence.context == context + assert error.evidence.authorization.outcome == "allow" + assert error.evidence.authorization.reason == "explicit_allow" + assert error.evidence.approval_status == "not_applicable" + assert error.evidence.execution_attempted is True + assert error.evidence.failure_reason == "executor_error" + assert error.evidence.external_side_effect_state == "unknown" + assert _RAW_EXECUTOR_ERROR not in error.evidence.model_dump_json() + assert _RAW_EXECUTOR_ERROR not in str(error) + assert isinstance(error.__cause__, RuntimeError) + assert str(error.__cause__) == _RAW_EXECUTOR_ERROR From cea0d2e0e5b57c8503e423265f89eed96bb774c9 Mon Sep 17 00:00:00 2001 From: Bruno Vicco Date: Sun, 6 Sep 2026 01:32:15 -0300 Subject: [PATCH 2/6] fix: preserve governed failure through Agno status --- .../adapters/agno/action_workflow.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/agentic_lab/adapters/agno/action_workflow.py b/src/agentic_lab/adapters/agno/action_workflow.py index b2ae1bb..80bac72 100644 --- a/src/agentic_lab/adapters/agno/action_workflow.py +++ b/src/agentic_lab/adapters/agno/action_workflow.py @@ -9,6 +9,7 @@ from agentic_lab.application.action_authorization import ActionContext, ProposedAction from agentic_lab.application.action_runtime import ( ActionExecutionEvidence, + GovernedActionExecutionError, GovernedActionRuntime, ) @@ -18,10 +19,11 @@ @dataclass(slots=True) class _AgnoGovernedActionState: - """Hold model-safe action state while Agno orchestrates execution.""" + """Bridge application-owned execution outcomes across Agno orchestration.""" proposed_action: ProposedAction execution_evidence: ActionExecutionEvidence | None = None + execution_error: GovernedActionExecutionError | None = None def _fail_closed_review() -> HumanReview: @@ -37,10 +39,15 @@ def _action_step( """Build one native Agno Step around the application-owned runtime.""" def execute(_: StepInput) -> StepOutput: - evidence = action_runtime.execute( - state.proposed_action, - context, - ) + try: + evidence = action_runtime.execute( + state.proposed_action, + context, + ) + except GovernedActionExecutionError as exc: + state.execution_error = exc + raise + state.execution_evidence = evidence return StepOutput(content=evidence) @@ -91,6 +98,8 @@ def run(self, proposed_action: ProposedAction) -> ActionExecutionEvidence: raw_output = workflow.run(input="governed action execution") if raw_output.status == RunStatus.error: + if state.execution_error is not None: + raise state.execution_error raise RuntimeError("Agno governed action Workflow execution failed") if raw_output.status != RunStatus.completed: raise RuntimeError( From 041aaf9d67ad83f21332470be55f4cd9c1efef0c Mon Sep 17 00:00:00 2001 From: Bruno Vicco Date: Sun, 6 Sep 2026 01:40:08 -0300 Subject: [PATCH 3/6] chore: add guarded phase53 docs sync --- .github/workflows/phase53-doc-sync.yml | 115 +++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .github/workflows/phase53-doc-sync.yml diff --git a/.github/workflows/phase53-doc-sync.yml b/.github/workflows/phase53-doc-sync.yml new file mode 100644 index 0000000..cb79971 --- /dev/null +++ b/.github/workflows/phase53-doc-sync.yml @@ -0,0 +1,115 @@ +name: phase53-doc-sync + +on: + push: + branches: + - fix/phase-53-agno-governed-failure-evidence + paths: + - .github/workflows/phase53-doc-sync.yml + +permissions: + contents: write + +jobs: + sync-docs: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - name: Update only Phase 53 documentation + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + def replace_once(text: str, old: str, new: str, label: str) -> str: + count = text.count(old) + if count != 1: + raise SystemExit(f"{label}: expected exactly one match, found {count}") + return text.replace(old, new, 1) + + changelog_path = Path("CHANGELOG.md") + changelog = changelog_path.read_text() + hardened_anchor = "- The trusted-composition governed MCP mutable boundary now applies the same fail-closed transport classification to post-executor `GovernedActionExecutionError`, preserving safe action-level failure evidence while keeping raw executor text out of MCP protocol data." + changelog = replace_once( + changelog, + hardened_anchor, + hardened_anchor + + "\n- Agno governed mutable execution now preserves the original `GovernedActionExecutionError` across `RunStatus.error` instead of replacing application-owned failure provenance with a generic framework error; the existing `max_retries=0` boundary remains unchanged.", + "CHANGELOG hardened", + ) + evidence_anchor = "- 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." + changelog = replace_once( + changelog, + evidence_anchor, + evidence_anchor + + "\n- 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.", + "CHANGELOG evidence", + ) + changelog_path.write_text(changelog) + + security_path = Path("docs/security/GOVERNED_AGENT_ACTIONS.md") + security = security_path.read_text() + old_section = """### Agno retry hardening + +The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior. + +This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. + +A regression test uses a failing executor and proves it is invoked exactly once. +""" + new_section = """### Agno retry and failure-provenance hardening + +The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior. + +This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. The zero-retry setting therefore remains explicit even though authorization and executor-failure semantics belong to the application runtime rather than Agno. + +A second boundary matters after that one allowed attempt fails. `GovernedActionRuntime` raises `GovernedActionExecutionError` with `ActionExecutionFailureEvidence`, but Agno represents the workflow itself as `RunStatus.error`. The adapter must not replace the application-owned governed failure with a new generic framework error merely because the workflow status is error. + +The Agno adapter therefore captures only `GovernedActionExecutionError` while the Step is executing and, when the Workflow reports `RunStatus.error`, re-raises that same governed exception. Non-governed Agno failures still use the adapter's generic framework error path. + +This preserves two independent facts: + +```text +Agno mutable executor failure + -> exactly one executor invocation + -> original GovernedActionExecutionError survives + -> ActionExecutionFailureEvidence remains inspectable +``` + +Regression coverage requires the exact caller context, proposed action and authorization evidence, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`. Raw executor exception text remains excluded from structured evidence and the governed error message; it remains available only as the local Python exception cause. + +This does not make Agno the owner of failure semantics and does not add retry, idempotency, rollback, compensation, or transaction guarantees. +""" + security = replace_once( + security, + old_section, + new_section, + "security Agno hardening section", + ) + security_path.write_text(security) + PY + - name: Guard documentation scope + shell: bash + run: | + git diff --check + mapfile -t changed < <(git diff --name-only | sort) + expected=( + CHANGELOG.md + docs/security/GOVERNED_AGENT_ACTIONS.md + ) + mapfile -t expected_sorted < <(printf '%s\n' "${expected[@]}" | sort) + if [[ "${changed[*]}" != "${expected_sorted[*]}" ]]; then + printf 'Unexpected changed files:\n%s\n' "${changed[*]}" + exit 1 + fi + - name: Commit documentation only + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md docs/security/GOVERNED_AGENT_ACTIONS.md + git diff --cached --check + git commit -m "docs: document Agno governed failure provenance" + git push From ca94f65137aeb5c00eb30ec07b7d18ca46ccb700 Mon Sep 17 00:00:00 2001 From: Bruno Vicco Date: Sun, 6 Sep 2026 01:43:00 -0300 Subject: [PATCH 4/6] chore: fix guarded phase53 docs sync --- .github/workflows/phase53-doc-sync.yml | 59 +++++++++++--------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/.github/workflows/phase53-doc-sync.yml b/.github/workflows/phase53-doc-sync.yml index cb79971..5c3ca04 100644 --- a/.github/workflows/phase53-doc-sync.yml +++ b/.github/workflows/phase53-doc-sync.yml @@ -35,53 +35,42 @@ jobs: changelog = replace_once( changelog, hardened_anchor, - hardened_anchor - + "\n- Agno governed mutable execution now preserves the original `GovernedActionExecutionError` across `RunStatus.error` instead of replacing application-owned failure provenance with a generic framework error; the existing `max_retries=0` boundary remains unchanged.", + hardened_anchor + "\n- Agno governed mutable execution now preserves the original `GovernedActionExecutionError` across `RunStatus.error` instead of replacing application-owned failure provenance with a generic framework error; the existing `max_retries=0` boundary remains unchanged.", "CHANGELOG hardened", ) evidence_anchor = "- 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." changelog = replace_once( changelog, evidence_anchor, - evidence_anchor - + "\n- 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.", + evidence_anchor + "\n- 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.", "CHANGELOG evidence", ) changelog_path.write_text(changelog) security_path = Path("docs/security/GOVERNED_AGENT_ACTIONS.md") security = security_path.read_text() - old_section = """### Agno retry hardening - -The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior. - -This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. - -A regression test uses a failing executor and proves it is invoked exactly once. -""" - new_section = """### Agno retry and failure-provenance hardening - -The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior. - -This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. The zero-retry setting therefore remains explicit even though authorization and executor-failure semantics belong to the application runtime rather than Agno. - -A second boundary matters after that one allowed attempt fails. `GovernedActionRuntime` raises `GovernedActionExecutionError` with `ActionExecutionFailureEvidence`, but Agno represents the workflow itself as `RunStatus.error`. The adapter must not replace the application-owned governed failure with a new generic framework error merely because the workflow status is error. - -The Agno adapter therefore captures only `GovernedActionExecutionError` while the Step is executing and, when the Workflow reports `RunStatus.error`, re-raises that same governed exception. Non-governed Agno failures still use the adapter's generic framework error path. - -This preserves two independent facts: - -```text -Agno mutable executor failure - -> exactly one executor invocation - -> original GovernedActionExecutionError survives - -> ActionExecutionFailureEvidence remains inspectable -``` - -Regression coverage requires the exact caller context, proposed action and authorization evidence, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`. Raw executor exception text remains excluded from structured evidence and the governed error message; it remains available only as the local Python exception cause. - -This does not make Agno the owner of failure semantics and does not add retry, idempotency, rollback, compensation, or transaction guarantees. -""" + old_section = ( + "### Agno retry hardening\n\n" + "The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior.\n\n" + "This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure.\n\n" + "A regression test uses a failing executor and proves it is invoked exactly once.\n" + ) + new_section = ( + "### Agno retry and failure-provenance hardening\n\n" + "The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior.\n\n" + "This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. The zero-retry setting therefore remains explicit even though authorization and executor-failure semantics belong to the application runtime rather than Agno.\n\n" + "A second boundary matters after that one allowed attempt fails. `GovernedActionRuntime` raises `GovernedActionExecutionError` with `ActionExecutionFailureEvidence`, but Agno represents the workflow itself as `RunStatus.error`. The adapter must not replace the application-owned governed failure with a new generic framework error merely because the workflow status is error.\n\n" + "The Agno adapter therefore captures only `GovernedActionExecutionError` while the Step is executing and, when the Workflow reports `RunStatus.error`, re-raises that same governed exception. Non-governed Agno failures still use the adapter's generic framework error path.\n\n" + "This preserves two independent facts:\n\n" + "```text\n" + "Agno mutable executor failure\n" + " -> exactly one executor invocation\n" + " -> original GovernedActionExecutionError survives\n" + " -> ActionExecutionFailureEvidence remains inspectable\n" + "```\n\n" + "Regression coverage requires the exact caller context, proposed action and authorization evidence, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`. Raw executor exception text remains excluded from structured evidence and the governed error message; it remains available only as the local Python exception cause.\n\n" + "This does not make Agno the owner of failure semantics and does not add retry, idempotency, rollback, compensation, or transaction guarantees.\n" + ) security = replace_once( security, old_section, From 16171d6fc1c48fd889cee225075e311f990883d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 04:43:08 +0000 Subject: [PATCH 5/6] docs: document Agno governed failure provenance --- CHANGELOG.md | 2 ++ docs/security/GOVERNED_AGENT_ACTIONS.md | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1958f66..a53f51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Authentication-first composition now re-wraps governed executor failures with the exact `credential_verified` evidence and rejects rejected-authentication or context-substitution combinations without copying credential or raw executor content. - Authenticated MCP mutable execution now maps only post-executor `AuthenticatedGovernedActionExecutionError` states to host-visible `MCPError` protocol failures, preventing an uncertain side-effect outcome from becoming an ordinary model-visible Tool error that invites self-directed retry. - The trusted-composition governed MCP mutable boundary now applies the same fail-closed transport classification to post-executor `GovernedActionExecutionError`, preserving safe action-level failure evidence while keeping raw executor text out of MCP protocol data. +- Agno governed mutable execution now preserves the original `GovernedActionExecutionError` across `RunStatus.error` instead of replacing application-owned failure provenance with a generic framework error; the existing `max_retries=0` boundary remains unchanged. ### Evidence @@ -38,6 +39,7 @@ - Exact isolated `mcp[cli]==2.1.1` compatibility and real STDIO checks prove uncertain authenticated mutable execution raises a protocol `MCPError`, not `CallToolResult(is_error=true)`, while preserving safe authenticated failure evidence in error `data`, zero observed fixture mutation for the failing resource, and a subsequent valid mutation. - 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. ## 1.3.0 - 2026-09-05 diff --git a/docs/security/GOVERNED_AGENT_ACTIONS.md b/docs/security/GOVERNED_AGENT_ACTIONS.md index 88d04e0..5ee233f 100644 --- a/docs/security/GOVERNED_AGENT_ACTIONS.md +++ b/docs/security/GOVERNED_AGENT_ACTIONS.md @@ -445,13 +445,28 @@ The framework orchestrates. The application decides and enforces. Existing framework adapters continue to use their explicitly injected local `trusted_composition` context. Phase 37 migrates their caller-policy fixtures to declare that source explicitly, so framework behavior cannot depend on an implicit source default inside authorization policy. Phase 47 keeps approver entitlement in the same shared application runtime; cross-framework conformance includes an unauthorized-approver scenario rather than duplicating HITL policy inside LangGraph, CrewAI, LlamaIndex, or Agno. API-key handling remains an application authentication concern rather than framework input. -### Agno retry hardening +### Agno retry and failure-provenance hardening The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior. -This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. +This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. The zero-retry setting therefore remains explicit even though authorization and executor-failure semantics belong to the application runtime rather than Agno. -A regression test uses a failing executor and proves it is invoked exactly once. +A second boundary matters after that one allowed attempt fails. `GovernedActionRuntime` raises `GovernedActionExecutionError` with `ActionExecutionFailureEvidence`, but Agno represents the workflow itself as `RunStatus.error`. The adapter must not replace the application-owned governed failure with a new generic framework error merely because the workflow status is error. + +The Agno adapter therefore captures only `GovernedActionExecutionError` while the Step is executing and, when the Workflow reports `RunStatus.error`, re-raises that same governed exception. Non-governed Agno failures still use the adapter's generic framework error path. + +This preserves two independent facts: + +```text +Agno mutable executor failure + -> exactly one executor invocation + -> original GovernedActionExecutionError survives + -> ActionExecutionFailureEvidence remains inspectable +``` + +Regression coverage requires the exact caller context, proposed action and authorization evidence, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`. Raw executor exception text remains excluded from structured evidence and the governed error message; it remains available only as the local Python exception cause. + +This does not make Agno the owner of failure semantics and does not add retry, idempotency, rollback, compensation, or transaction guarantees. ## 8. Cross-framework conformance From fafc5aea0ca666d0abd5dbd07c89689262d83dce Mon Sep 17 00:00:00 2001 From: Bruno Vicco Date: Sun, 6 Sep 2026 01:44:13 -0300 Subject: [PATCH 6/6] chore: remove temporary phase53 docs workflow --- .github/workflows/phase53-doc-sync.yml | 104 ------------------------- 1 file changed, 104 deletions(-) delete mode 100644 .github/workflows/phase53-doc-sync.yml diff --git a/.github/workflows/phase53-doc-sync.yml b/.github/workflows/phase53-doc-sync.yml deleted file mode 100644 index 5c3ca04..0000000 --- a/.github/workflows/phase53-doc-sync.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: phase53-doc-sync - -on: - push: - branches: - - fix/phase-53-agno-governed-failure-evidence - paths: - - .github/workflows/phase53-doc-sync.yml - -permissions: - contents: write - -jobs: - sync-docs: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - - name: Update only Phase 53 documentation - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - def replace_once(text: str, old: str, new: str, label: str) -> str: - count = text.count(old) - if count != 1: - raise SystemExit(f"{label}: expected exactly one match, found {count}") - return text.replace(old, new, 1) - - changelog_path = Path("CHANGELOG.md") - changelog = changelog_path.read_text() - hardened_anchor = "- The trusted-composition governed MCP mutable boundary now applies the same fail-closed transport classification to post-executor `GovernedActionExecutionError`, preserving safe action-level failure evidence while keeping raw executor text out of MCP protocol data." - changelog = replace_once( - changelog, - hardened_anchor, - hardened_anchor + "\n- Agno governed mutable execution now preserves the original `GovernedActionExecutionError` across `RunStatus.error` instead of replacing application-owned failure provenance with a generic framework error; the existing `max_retries=0` boundary remains unchanged.", - "CHANGELOG hardened", - ) - evidence_anchor = "- 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." - changelog = replace_once( - changelog, - evidence_anchor, - evidence_anchor + "\n- 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.", - "CHANGELOG evidence", - ) - changelog_path.write_text(changelog) - - security_path = Path("docs/security/GOVERNED_AGENT_ACTIONS.md") - security = security_path.read_text() - old_section = ( - "### Agno retry hardening\n\n" - "The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior.\n\n" - "This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure.\n\n" - "A regression test uses a failing executor and proves it is invoked exactly once.\n" - ) - new_section = ( - "### Agno retry and failure-provenance hardening\n\n" - "The Agno mutable step explicitly uses `max_retries=0` and fail-closed step behavior.\n\n" - "This is security-relevant because an implicit framework retry around a mutating executor could multiply side effects after a partial failure. The zero-retry setting therefore remains explicit even though authorization and executor-failure semantics belong to the application runtime rather than Agno.\n\n" - "A second boundary matters after that one allowed attempt fails. `GovernedActionRuntime` raises `GovernedActionExecutionError` with `ActionExecutionFailureEvidence`, but Agno represents the workflow itself as `RunStatus.error`. The adapter must not replace the application-owned governed failure with a new generic framework error merely because the workflow status is error.\n\n" - "The Agno adapter therefore captures only `GovernedActionExecutionError` while the Step is executing and, when the Workflow reports `RunStatus.error`, re-raises that same governed exception. Non-governed Agno failures still use the adapter's generic framework error path.\n\n" - "This preserves two independent facts:\n\n" - "```text\n" - "Agno mutable executor failure\n" - " -> exactly one executor invocation\n" - " -> original GovernedActionExecutionError survives\n" - " -> ActionExecutionFailureEvidence remains inspectable\n" - "```\n\n" - "Regression coverage requires the exact caller context, proposed action and authorization evidence, `execution_attempted=true`, `failure_reason=executor_error`, and `external_side_effect_state=unknown`. Raw executor exception text remains excluded from structured evidence and the governed error message; it remains available only as the local Python exception cause.\n\n" - "This does not make Agno the owner of failure semantics and does not add retry, idempotency, rollback, compensation, or transaction guarantees.\n" - ) - security = replace_once( - security, - old_section, - new_section, - "security Agno hardening section", - ) - security_path.write_text(security) - PY - - name: Guard documentation scope - shell: bash - run: | - git diff --check - mapfile -t changed < <(git diff --name-only | sort) - expected=( - CHANGELOG.md - docs/security/GOVERNED_AGENT_ACTIONS.md - ) - mapfile -t expected_sorted < <(printf '%s\n' "${expected[@]}" | sort) - if [[ "${changed[*]}" != "${expected_sorted[*]}" ]]; then - printf 'Unexpected changed files:\n%s\n' "${changed[*]}" - exit 1 - fi - - name: Commit documentation only - shell: bash - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add CHANGELOG.md docs/security/GOVERNED_AGENT_ACTIONS.md - git diff --cached --check - git commit -m "docs: document Agno governed failure provenance" - git push