fix(security): active guard enforcement, CEO A2A target check, notification expiry (#595)

* fix(security): guard goes active; CEO A2A respects no-comms roles; ack notifications expire

ROBOCO_GUARD_PASSIVE_MODE defaults to false in both compose files — the
deferred post-calibration flip; fail_secure stays off and the env override
remains the rollback. can_a2a_direct no longer short-circuits the CEO past
the no-comms set (auditor/pr_reviewer/prompter/secretary), now canonical
in foundation.policy.communications.NO_COMMS_ROLES and shared with the
content-actions gate; the A2A service refuses at conversation creation
instead of silently suppressing the wake. Ack-required notifications get
expires_at stamped from ROBOCO_NOTIFICATION_ACK_TTL_HOURS (default 48,
0 disables), so the re-escalation sweeper's expires_at query matches rows
for the first time.

* refactor(notification): extract _ack_and_expiry — xenon rank back under B

The expires_at stamping pushed _create_notification_with_session to
rank C; the requires_ack + expiry derivation moves into a helper with
the same semantics and comments.

* test(conftest): dispose the global DB engine after every test

Production code reaching get_db_context()/get_engine() lazily creates the
process-global engine bound to the current event loop; with per-test
function-scoped loops, any later test touching the global path inherits a
dead-loop engine and dies with 'Future attached to a different loop' —
the order-dependent class that has been wandering the suite (cloud_auth
login, metrics, tasks-routes, full-lifecycle) whenever collection order
shifts. An autouse fixture now close_db()s after every test, keeping the
global path loop-local; no-op when untouched.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-19 18:46:44 +02:00
committed by GitHub
co-authored by Renn F
parent 5b27a443e9
commit fc41dfa40e
18 changed files with 445 additions and 65 deletions
+19 -1
View File
@@ -36,6 +36,7 @@ Redis isolation:
from __future__ import annotations
import contextlib
import json
import os
import socket
@@ -48,7 +49,7 @@ import pytest
import pytest_asyncio
from roboco.config import settings as _settings
from roboco.db import tables as roboco_tables
from roboco.db.base import Base
from roboco.db.base import Base, close_db
from roboco.db.tables import (
AgentTable,
AuditLogTable,
@@ -78,6 +79,23 @@ if TYPE_CHECKING:
from collections.abc import AsyncIterator
@pytest_asyncio.fixture(autouse=True)
async def _dispose_global_db_engine() -> AsyncIterator[None]:
"""Never let the lazy global engine outlive the test that created it.
Production code reaching ``get_db_context()``/``get_engine()`` creates
the process-global ``_DbHolder`` engine bound to the CURRENT event loop.
With function-scoped test loops, any later test touching that global
path inherits an engine from a dead loop and crashes with ``Future
attached to a different loop`` — an order-dependent failure class that
moves around whenever test collection shifts. Disposing after every
test keeps the global path loop-local; a no-op when nothing touched it.
"""
yield
with contextlib.suppress(Exception):
await close_db()
@pytest.fixture(autouse=True)
def _no_live_redis(monkeypatch: pytest.MonkeyPatch) -> None:
"""Keep every test off the real localhost Redis (see module docstring).