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
+2 -2
View File
@@ -11,7 +11,7 @@ RoboCo's HTTP request layer is protected by `fastapi-guard` (v7.2.1), implemente
| `ROBOCO_GUARD_ENABLED` | `false` | Master switch. Off = completely inert — no middleware is mounted, the request path is entirely unchanged, and nothing is logged or blocked. |
| `ROBOCO_GUARD_PASSIVE_MODE` | see below | When the guard is enabled, controls whether it blocks matching requests or only logs them. |
As of 2026-07-01 the guard is built on the `feature/fastapi-guard-hardening` branch, gated off by default, and wherever it is enabled at all it is running in passive/log-only mode — so no agent request is currently being blocked by it anywhere.
As of 2026-07-19 the guard is gated off by default in config, but the NAS build compose arms it ON in ACTIVE enforcement (`ROBOCO_GUARD_PASSIVE_MODE=false`) — passive/log-only calibration came back clean, and the CEO approved the flip now that cloud auth + Tailscale are armed. A matching request on that deploy is actually blocked, not just logged. The registry compose still ships it fully off (see Enforcement Posture below).
## When Armed
@@ -27,7 +27,7 @@ On top of those generic checks, three RoboCo-specific custom validators run agai
## Enforcement Posture
`ROBOCO_GUARD_PASSIVE_MODE` decides what happens on a match: `true` (passive) detects and logs only, and never blocks a request — this is how the NAS production deploy is armed today. `false` (enforce) actually blocks the matching request.
`ROBOCO_GUARD_PASSIVE_MODE` decides what happens on a match: `true` (passive) detects and logs only, and never blocks a request. `false` (enforce) actually blocks the matching request — this is how the NAS build compose is armed today (its default flipped from `true` to `false` once passive-mode calibration reviewed clean). The registry compose omits the guard trio entirely, leaving a fresh third-party deploy on the safe config default (guard off).
A blocked request gets a generic `400` or `403` response — no rule or signature detail is returned, so the response body can't be used to fingerprint what tripped the guard.
@@ -6,7 +6,7 @@ A call to the orchestrator API fails with a generic `400` or `403` and no detail
## Cause
RoboCo's HTTP security layer (`fastapi-guard`; see `docs/rag/architecture/http-security-guard.md`) rejected the request. This can only happen when both `ROBOCO_GUARD_ENABLED=true` and `ROBOCO_GUARD_PASSIVE_MODE=false` (enforce mode) are set. As of 2026-07-01 the guard is off by default, and wherever it is enabled it runs passive/log-only, so this is not something that occurs today — it's documented so the block is recognizable if/when enforcement is turned on later.
RoboCo's HTTP security layer (`fastapi-guard`; see `docs/rag/architecture/http-security-guard.md`) rejected the request. This can only happen when both `ROBOCO_GUARD_ENABLED=true` and `ROBOCO_GUARD_PASSIVE_MODE=false` (enforce mode) are set. As of 2026-07-19 that is the default on the NAS build compose — a matching request is actually blocked there, not just logged.
The guard never returns which rule or signature matched, by design, so the 400/403 body itself gives you nothing to act on. Avoiding the triggers below is the only real mitigation.
@@ -14,18 +14,22 @@ The guard never returns which rule or signature matched, by design, so the 400/4
Generic WAF signatures (SQL injection, XSS, path traversal, suspicious URL patterns) are excluded from scanning on the free-text body fields of `note` / `commit` / `dm`, so normal code, SQL, diffs, file paths, and URLs in those bodies are safe from that layer. Three custom validators scan those same bodies regardless of that exclusion:
- Prompt-injection detection
- Secret-exfil detection — literal credential-shaped strings (`sk-ant-...`, `ghp_...`, postgres connection URLs) or phrasing like "reveal your api keys"
- Prompt-injection detection — phrasing like "ignore previous instructions", or "bypass/disable/override the guard/filter/restriction" (a real risk in this repo's own security-work commit messages and notes: `roboco/security.py`'s own doctring vocabulary uses "guard", "bypass", "disable" constantly)
- Secret-exfil detection — literal credential-shaped strings (`sk-ant-...`, `ghp_...`, postgres connection URLs) **or the literal doc pattern `ROBOCO_ENCRYPTION_KEY=<...>` / `ROBOCO_AGENT_AUTH_SECRET=<...>` / `FERNET_KEY=<...>`** (matches `CLAUDE.md` and `.env.example`'s own env-var documentation verbatim — quoting or editing those lines in a `note`/`commit` body trips this) or phrasing like "reveal your api keys"
- Internal-SSRF detection — fetch-type bodies targeting internal or metadata hosts (`169.254.169.254`, `roboco-*` internal service hostnames)
The three custom validators scan the raw request body regardless of which top-level field the text sits in — unlike the WAF's field exclusion, there is no safe field for these three.
## Solution: Hygiene Rules
Follow these when composing `note` / `commit` / `dm` bodies or any fetch-type payload, regardless of whether enforcement is currently active:
Follow these when composing `note` / `commit` / `dm` bodies or any fetch-type payload:
1. Never paste real secrets or credentials (API keys, tokens, DB connection strings) into a request body, even inside a code snippet or diff.
2. Never aim a fetch/HTTP-call body at an internal service host (`roboco-*`) or a cloud metadata endpoint (`169.254.169.254`).
3. Code, SQL, diffs, file paths, and HTML snippets are otherwise fine to include — the WAF layer is calibrated to exclude legitimate agent content on those fields.
2. When discussing an env var like `ROBOCO_ENCRYPTION_KEY` in a note/commit body, don't write it as `NAME=value` (even a placeholder value) — write `ROBOCO_ENCRYPTION_KEY` and describe the value separately, e.g. "set to a generated Fernet key", to avoid the `NAME=<10+ chars>` credential-shape match.
3. Avoid "bypass/override/disable the guard/filter/restriction/safety" phrasing in commit messages or notes about this security layer itself — describe the change without that verb+noun adjacency (e.g. "excludes X from the WAF scan" instead of "bypasses the guard's WAF scan").
4. Never aim a fetch/HTTP-call body at an internal service host (`roboco-*`) or a cloud metadata endpoint (`169.254.169.254`).
5. Code, SQL, diffs, file paths, and HTML snippets are otherwise fine to include — the WAF layer is calibrated to exclude legitimate agent content on those fields (this exclusion does not cover the three custom validators above).
## Current Status (2026-07-01)
## Current Status (2026-07-19)
The guard is built on the `feature/fastapi-guard-hardening` branch, off by default (`ROBOCO_GUARD_ENABLED=false`), and wherever enabled it runs in passive/log-only mode. No agent request is being blocked by it today — the rules above are about good hygiene now and correctness later, not a live restriction.
`ROBOCO_GUARD_ENABLED` is off by default in config; the NAS build compose arms it ON with `ROBOCO_GUARD_PASSIVE_MODE=false` (enforce). A matching request on that deploy is genuinely blocked, not just logged — the rules above are a live restriction there, not just future-proofing. The registry compose and a bare config default both stay off.