Skip to content

Fix/lru eviction and effective fingerprint - #2

Merged
nayfly merged 2 commits into
mainfrom
fix/lru-eviction-and-effective-fingerprint
Jul 27, 2026
Merged

nayfly merged 2 commits into
mainfrom
fix/lru-eviction-and-effective-fingerprint

Conversation

@nayfly

@nayfly nayfly commented Jul 27, 2026

Copy link
Copy Markdown
Owner

No description provided.

Robert and others added 2 commits July 26, 2026 16:22
…-Redis suite

Follow-up on review findings. Two were correct against what I had written, and
the first turned out to be exploitable rather than merely overstated.

Rate limiter eviction could reset a throttled window. A throttled client backs
off -- correct client behaviour -- which makes it the least-recently-used entry,
so plain LRU dropped it first and its limit reset. Measured: 2000 junk actor ids
restored a blocked actor to allowed. Windows at or above the limit are now never
evicted; when nothing evictable remains a new actor is refused instead
(fail-closed) and counted in capacity_refusals. The docstring claimed "active
actors are never displaced", which the algorithm did not guarantee; it now
describes what it actually does.

The policy pin compared file bytes, not behaviour. Hashing the YAML is wrong in
both directions: reformatting or editing a comment rejected a behaviourally
identical policy, and a code upgrade changing a default left the file untouched
so the hash was unchanged while the engine decided differently -- exactly the
silent divergence the pin exists to catch. effective_fingerprint hashes the
loaded config with defaults resolved and is what the pin checks;
policy_fingerprint is kept for provenance. Both on /health and in the new
`aetherya policy fingerprint`.

Real-Redis integration suite with a service container in CI. The in-memory
double cannot demonstrate MULTI/EXEC semantics, serialization between concurrent
clients, real TTL, or that separate instances share state. Validated against
Redis 6.2.14: under 100 threads a deliberately racy read-then-write granted 92
permits against a limit of 10 where this implementation granted exactly 10, so
the test discriminates rather than passing vacuously.

Durable audit sinks. A protocol alone did not close the retention gap.
HTTPAuditSink (batched, background worker, bounded queue with oldest-drop,
retry) and FileAuditSink ship byte-identical lines. /health now reports
audit_mirror_ok, errors, dropped, pending and last success, backed by a
process-level registry since AuditLogger is built per request -- shipping is
best-effort by design, so alerting on those counters is the only thing
separating durable audit from a local file that looks durable.

Also fixes a deadlock I introduced in the sink: stats() held the stats lock and
called healthy(), which took the non-reentrant lock again, while pending() took
the queue lock in the opposite order to write(). And close() flushed one batch,
losing the tail on shutdown; it now drains.

Verification: 1054 unit tests, 100% coverage, mypy strict and ruff clean.
13 integration tests green against real Redis. pipeline p95 0.23ms,
chaos p95 1.29ms detection 1.0, security_baseline 0 differences.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The phases were a sketch; this makes them executable, and adds the thing that
was actually missing to picture the system working.

docs/rollout-phases.md states each phase's posture, the wiring change it needs,
what to measure, and exit criteria that can fail — including what to do when
shadow mode reports a false-positive rate you cannot accept, and the fact that
any policy change restarts the measurement window because the fingerprint moved.

examples/agent_loop.py runs an actual loop. The existing example showed the API
call but not what an agent is: here the model proposes a tool call, the engine
rules on it, and the result or the refusal goes back into the conversation. The
--phase flag switches enforcement posture without touching policy or wiring,
which is the property that makes shadow-then-tighten safe.

The scenario is a prompt injection arriving through tool output, not a malicious
model — the agent reads a poisoned document and proposes what it was told to.
That is the case the boundary exists for and the model cannot solve alone.

Two backends: a deterministic scripted agent (no key, no network, reproducible)
and a real OpenAI tool-calling loop. The gating code around them is identical.
The OpenAI path is unverified here — no API key was available — and is marked as
such in the source.

Verification: 1065 tests, 100% coverage, mypy strict and ruff clean. All three
phases exercised by tests, including that enforcement tightens monotonically and
that refusals are fed back rather than raised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nayfly
nayfly merged commit 6316c24 into main Jul 27, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant