Release Notes¶
v3.0.0 (2026-09-04)¶
Recursive header and metadata redaction, an atomic block-policy buffer, and a maintainability split (v3.0.0)¶
Breaking Changes¶
BufferProtocol.requeue_events_in_memoryandrequeue_metrics_in_memoryare now async. A failed send's requeue now shares the same lock as every other buffer mutation, so both methods on the protocol (and onEventBuffer) must be awaited. A customBufferProtocolimplementation needs to change its signatures toasync def;GuardAgentHandler's own buffer andFlushMixinalready call them withawait.sanitize_headersnow redacts what it cannot safely classify instead of letting it through. A JSON-looking string over 8192 characters, a value of a type it does not recognize (a generator, an arbitrary object), and a bytes header key that fails to decode as UTF-8 previously passed through unchanged or matched nothing; all three now become[REDACTED]. A payload that relied on that pass-through will see[REDACTED]there instead.
Added¶
sanitize_headersrecurses into every container and into JSON-looking strings, including a double-encoded JSON string. Dicts, lists, tuples, sets, frozensets and JSON string bodies are all walked (up to a bounded depth), not just the top-level mapping; a JSON string whose decoded value is itself a JSON string is unwrapped and sanitized too, then re-encoded the same way.datetime,date,time,Decimal,UUIDandEnumvalues pass through unchanged. They are legitimate scalar metadata, not unrecognized objects to redact.- A dataclass instance or a pydantic model passed as metadata is walked field by field, like a mapping, via
dataclasses.asdict/model_dump, instead of being collapsed to[REDACTED]wholesale. - The default
sensitive_headersset now matches guard-core's:proxy-authorizationjoinsauthorization,cookieandx-api-key. KNOWN_EVENT_TYPESgainedip_ban_failed,rate_limit_script_reloaded,pattern_anomaly_timeout,pattern_anomaly_statistical_anomalyandroute_unresolved, aligning with guard-core 4.0.0's event catalogue. A version-gated test skips until the installedguard_corereaches 4.0.0 and fails on any other drift once it does.tests/test_maintainability_rank.pyenforces radon MI rank A on every module inguard_agent.on_erroralso fires at the flush boundary (stage='flush_events'/'flush_metrics'), once per failed batch, with the exception and{"batch_size": N}, through the samefire_error_hookhelperHTTPTransportuses. Previously it only fired inside the transport's own retry paths.
Fixed¶
- A bytes header key no longer bypasses the sensitive-header match.
b"authorization"is decoded as UTF-8 before the case-insensitive comparison instead of being stringified to the literal"b'authorization'", which never matched anything. - The event and metric buffers each serialize their overflow-check-and-append, flush, requeue and clear through one
asyncio.Lock. Two concurrentadd_event(oradd_metric) calls at capacity could previously both evict the same slot, appending past it and orphaning the other call's Redis record; the check and the append are now one atomic step. - The block overflow policy waits on an
asyncio.Conditionbound to that same lock and re-checks at least once every 0.5 seconds. A requeue after a failed send always keeps its slot (durability over a new writer), and a blocked writer no longer depends solely on an explicit signal that a send-failure backoff (up to 300 seconds) can delay past. clear_buffernow clears the Redis-key maps along with the buffers. Previously only the deques were cleared, so a later event or metric object could inherit a stale or foreign key once CPython reused itsid().- A Redis record is deleted on a requeue eviction and on an overflow drop without racing the buffer it was tracked against.
- The Makefile's
semgreptarget scansguard_agentwith--no-git-ignore, so a new, not-yet-tracked file is scanned deterministically instead of depending on git state. - A
BaseExceptionfrom the transport (anasyncio.CancelledError, not anExceptionsubclass) no longer skips the batch's requeue and failure bookkeeping._flush_events/_flush_metricscatchBaseException, requeue the popped batch and advance the failure streak and backoff before re-raising, so a cancellation still propagates but never silently drops the batch. - Loading persisted events/metrics from Redis at startup no longer races live
add_event/add_metrictraffic. Each load now runs under its buffer's own condition lock, and forgets the oldest tracked key before its own append would otherwise evict an item past capacity, so a startup load can never orphan a Redis record or corrupt the key map racing a concurrent write.
Internal (v3.0.0)¶
buffer.py,client.pyandtransport.pywere split into focused mixins (_buffer_lifecycle,_buffer_overflow,_buffer_queue,_buffer_redis;_client_flush,_client_ingest,_client_loops,_client_status;_transport_dispatch,_transport_lifecycle,_transport_send), each independently testable and each rank A.anyio<4.15andsemgrepwere added to thedevextra.
v2.10.0 (2026-09-01)¶
Agent logs that identify themselves (v2.10.0)¶
Added¶
- guard-agent log lines now carry an origin prefix.
setup_agent_loggingattaches guard-core's standard formatter ([guard_agent.client] 2026-09-01 12:00:00 - WARNING - ...) to theguard_agentlogger tree, so agent records such asEvents flush recovered after N consecutive partial failure(s)are identifiable in hosted log viewers instead of arriving bare. It runs automatically when aGuardAgentHandlerorSyncGuardAgentHandleris constructed, and the function is exported for explicit host use:setup_agent_logging(log_file=..., log_format="json"). - JSON output and an optional file sink.
log_format="json"emits{"timestamp", "level", "logger", "message"};log_file=...adds aFileHandleralongside the console handler. A console handler is only attached when the root logger has no handlers, matching guard-core's yield-to-host contract, so records still reach host-configured logging through propagation.
Changed¶
- The automatic setup is non-destructive: hosts keep their logging configuration. Constructing the handler attaches at most one console handler, never changes a level the host set, and never removes a host-configured file handler or formatter, no matter how many times the handler is constructed. Hosts reconfigure by calling
setup_agent_logging(...)explicitly, which re-applies intent (agent handlers are cleared first). - The old async
setup_agent_loggingstub inguard_agent.utilsis gone. It was never called and had zero consumers across the ecosystem, and its export now points at the real implementation inguard_agent.logging_utils. The signature changed fromasync (log_level: str)to(log_file: str | None, log_format: str, *, reconfigure: bool).
Internal (v2.10.0)¶
- pytest now runs with
filterwarnings = ["error"], so any warning, including one raised at import/collection time, fails the suite instead of passing silently. - That escalation surfaced a real resource leak:
tests/test_adapter_fastapi.py'sSecurityConfigdidn't disable Redis, leaving an unclosed async Redis socket to raise aResourceWarningat a later, unrelated test's garbage collection. Addedenable_redis=False, the same fix already applied totest_performance.pyin v2.3.0 to eliminate ResourceWarning pollution.
v2.9.1 (2026-08-28)¶
Carry the auto-ban overrides guard-core 3.13.0+ reads off DynamicRules (v2.9.1)¶
Fixed¶
DynamicRulesnow definesauto_ban_threshold,auto_ban_durationandenable_rate_limit_auto_ban. guard-core 3.13.0 started reading all three during rule application, but this model never declared them (Pydantic defaultextra=ignoredropped them from the backend payload), so on guard-core 3.13.0 and later every poll ended inFailed to apply dynamic rules: 'DynamicRules' object has no attribute 'enable_rate_limit_auto_ban'and guard-core restored its pre-apply config snapshot, discarding the rest of the push. Reproduced live on guard-core 3.15.0 against a fresh project before any rule was pushed. All three are optional overrides defaulting toNone, so guard-core skips them when unset and applies them when the backend sends them (#44).- The two integer overrides are floored at
1. guard-core's ownDynamicRulesmirror and itsSecurityConfigrevalidator rejectauto_ban_thresholdandauto_ban_durationbelow1, so a push carrying0would have failed mid-apply and rolled the whole rule back the same way. The floor is enforced at parse time instead.
v2.9.0 (2026-08-25)¶
Carry guard_core_version on telemetry so the SaaS can identify vulnerable guard-core releases (v2.9.0)¶
Added¶
AgentConfigandEventBatchnow carryguard_core_version, andHTTPTransportsends it on all three send paths. guard-core 3.13.0 setsguard_core_versionto the runningguard_core.__version__when it builds the agent config at init, so the SaaS can identify deployments running a vulnerable guard-core release independently of the wrapper's own version. guard-agent 2.8.1 had no such field and silently dropped the value (Pydantic defaultextra=ignore), so it never reached the platform. The field is now accepted, carried on the twoEventBatchconstructions and the encrypted-payload dict, and sent alongsideguard_version.
v2.8.1 (2026-08-09)¶
Documentation accuracy sweep: six snippets showed middleware that never attaches (v2.8.1)¶
Fixed¶
- Six documented snippets built a middleware that was never installed.
SecurityMiddleware(app, config=config)constructs the middleware and discards it; onlyapp.add_middleware(SecurityMiddleware, config=config)installs it, which is observable aslen(app.user_middleware)staying at zero. This affecteddocs/tutorial/getting-started.mdin three places, including the block labelled "The recommended deployment", plusdocs/api/overview.md,docs/installation.md's configuration-verification script (which printed a success message regardless of whether anything was wired), andexamples/basic_usage.py. Anyone copying them ran an application with no security middleware and no telemetry, with nothing to indicate it. examples/basic_usage.pypresented the duplicate-singleton anti-pattern as recommended. It hand-built anAgentConfigand wired its own lifespan, which creates an agent handler that never receives traffic and leaves the dashboard empty. It now uses the supported path, whereSecurityConfig'sagent_*fields drive the agent the middleware itself builds.buffer_sizeguidance contradicted itself. The README showed5000while the in-wheel skill said keep the default of 100. The 256 KiB request body cap is real and enforced server side, and no client side batch count limit exists anywhere in the agent. All surfaces now agree on 100.- The pydantic plugin mute was described as unconditional. It is not. An
ImportErroronguard_agent.modelsreturns silently with nothing logged, all three models share onetry/exceptso a failure on the first leaves the rest unmuted, and it runs once at import with no retry, making a failure permanent for the process. The documentation now states what actually holds, and describes the mute in the README anddocs/index.mdfor the first time. make lint-docswas scanning almost nothing. Repeated-eflags do not accumulate in pymarkdownlnt, so every exclusion but the last was silently discarded, and YAML front matter was being misparsed as heading content. Exclusions moved to[tool.pymarkdown.system] exclude_path, with the package directory deliberately left in scope so the shipped in-wheel skill is linted. The gate now exits zero against a real scan.guard_agent.__version__reported the wrong version.guard_agent/_version.pywas left at2.7.1when 2.8.0 was released, whilepyproject.tomlsaid2.8.0. That file is the single source of truth imported byguard_agent.__init__, and it feeds the transport's User-Agent header andEventBatch.agent_version, so telemetry emitted by 2.8.0 identified itself as 2.7.1 to the platform. It is now2.8.1, matching the package metadata.- Rendered list structure in the troubleshooting and validation sections. Code fences separated from their list items by a blank line need a four space indent for python-markdown to keep them inside the item; at three spaces each item fractured into its own single-item list with the fence orphaned beside it. Neither pymarkdownlnt nor
mkdocs build --strictdetects this, so it was verified against the built HTML.
Documentation¶
- Roughly sixteen further corrections across the README,
docs/, the in-wheel skill and the example, each verified against the code as shipped rather than against neighbouring documentation. No runtime behaviour changed in this release.
v2.8.0 (2026-08-03)¶
Split-or-drop on 413, drop on permanent 4xx, standalone logfire mute, library-skills skill (v2.8.0)¶
Fixed¶
- 413 poison flush loop closed. The SaaS platform caps request bodies at 256 KiB, so an oversized batch always 413s; guard-agent retried 4xx blindly and requeued the whole batch, producing an infinite serialize+encrypt+POST loop on the event loop. On
PayloadTooLargeErrorthe batch is now halved and each half retried recursively; a single item that still 413s is dropped (it will never fit). Permanent 4xx (auth, quota, etc.) drops the whole batch via_drop_permanent_rejectioninstead of requeuing forever. - Permanent 4xx no longer trips the circuit breaker. A permanent 4xx is a healthy server rejecting a payload, not a transient failure.
CircuitBreaker.callnow re-raisesPermanentClientErrorbefore theexcept Exceptionthat incrementsfailure_count. Without this, five consecutive 413s opened the breaker; split halves then hit the plainException("Circuit breaker is OPEN"), which_send_with_retrytreats as transient, flippingreturn left and righttoFalseand requeuing the entire original batch forever. The split fix alone did not close the loop; this one-line guard in the shared function fixes every caller.
Added¶
- Standalone logfire mute. When guard-core is not imported, guard-agent now mutes its own telemetry pydantic models (
SecurityEvent/SecurityMetric/EventBatch) at import via pydanticplugin_settings, mirroring guard-core v3.8.1's mute. Closes the standalone-guard-agent edge case where a host's bareinstrument_pydantic()emitted guard-event validation spans. - Library-skills skill embedded at
guard_agent/.agents/skills/guard-agent/SKILL.mdsouvx library-skills --claudediscovers guard-agent from the installed wheel.
Internal¶
examples/basic_usage.pyimport-path, async-handler, and typing bugs fixed.
Behaviour changes¶
- Oversized batches that 413 are now split and dropped instead of requeued forever; permanent-4xx batches are dropped instead of retried. Callers that relied on infinite requeue will see batches drop (the intended behavior).
v2.7.1 (2026-07-30)¶
Bounded response-body logging and partial-failure backoff (v2.7.1)¶
Fixed¶
- Unbounded response bodies no longer flood logs. A branded HTML maintenance page served during a 5xx window caused
response.text— the full, untruncated body — to be embedded verbatim in exception messages and log lines on every retry, filling a customer's hosting logs with thousands of lines of HTML and inline CSS per request. Newsummarize_response_body()(guard_agent/utils.py) collapses all whitespace/newlines into a single line and caps the summary at 300 characters with a truncation-plus-original-length indicator, and is now used at every site inHTTPTransport._handle_responsewhere a response body reached a log line or exception message: the non-retryable 4xx path (PermanentClientErrordetail and its log line), the 5xx path (which now also includes the URL, previously missing), and the generic client-error log path. Status code and URL are always kept in the message. - A permanently-rejected 200 (e.g. quota exceeded) no longer retries every 30 seconds forever. The SaaS platform can answer HTTP 200 with
success=Falsefor conditions a retry cannot fix (e.g."Event quota exceeded. Upgrade your plan."), which the existing 4xxPermanentClientErrorhandling does not cover, so the agent re-sent and re-logged the same rejected batch on every flush indefinitely.GuardAgentHandlernow tracks consecutive partial failures per data type (events/metrics) and backs off the next attempt withcalculate_backoff_delay(capped at 5 minutes), resetting on the first success — no reliance on parsing the error message, so it also covers future rejection reasons. The "failed to send" and "recovered" log lines now fire once per streak transition instead of once per flush. Buffered events/metrics are left untouched during the backoff window — never dropped beyond the buffer's existing overflow policy — and are retried once the backoff elapses.
Internal¶
- Tests added in
tests/test_client_backoff.py,tests/test_transport.py, andtests/test_utils.pycovering the bounded-body summarizer (short text unchanged, whitespace/newline collapsing, truncation with original length) and the backoff behavior (skips repeat attempts, logs once per transition, resets on success, preserves buffered events/metrics across the backoff window). Full suite at 406 passed / 2 skipped, coverage maintained at 100% line + 100% branch.
v2.7.0 (2026-06-23)¶
Observable transport errors and documented Protocols (v2.7.0)¶
Added¶
AgentConfig.on_errorhook for observable delivery failures. New optionalon_error: Callable[[str, BaseException, dict[str, Any]], None].HTTPTransportfires it at the real failure points — serialization/encryption (stage="encryption"), unencrypted serialization and delivery (stage="transport_send"), on a permanent client error, and after retry exhaustion — so a host can observe that telemetry could not be shipped. The hook is best-effort and guaranteed never to propagate into the send path: a hook that raises is caught and logged, never destabilizing the application.
Documentation¶
- Documented the integrator-facing Protocols.
RedisHandlerProtocol,TransportProtocol,BufferProtocol, andAgentHandlerProtocolupgraded from thin one-line class docstrings to WHAT/WHEN/HOW class contracts plus a per-method docstring on every method, documenting the previously implicit semantics:send_*returnsboolmeaning accepted (caller requeues onFalse),None-on-miss for reads, the buffer's drain → confirm-on-success / requeue-on-failure at-least-once handshake, and TTL in seconds. Docstrings only — no signature, name, or@runtime_checkablechange.
Internal¶
- Refactored
_send_with_retry/_handle_responseinto smaller helpers (_evaluate_send_result,_sleep_or_record_giveup,_handle_200) to satisfy the complexity gate, and enabled branch coverage. Behavior-preserving.
v2.6.0 (2026-05-12)¶
Configurable rules and status loop intervals (v2.6.0)¶
- Added —
AgentConfig.dynamic_rule_interval: int(default 300, ge=60) — interval in seconds between dynamic rule polls. - Added —
AgentConfig.status_interval: int(default 300, ge=60) — interval in seconds between agent status reports. - Changed —
_rules_loopnow sleepsself.config.dynamic_rule_intervalinstead of a hardcoded300._status_loopnow sleepsself.config.status_intervalinstead of a hardcoded300. Both loops were previously ignoring any caller-configured value, soSecurityConfig.dynamic_rule_interval(and the newSecurityConfig.agent_status_intervalin guard-core >= 3.1.0) had no effect on the agent's poll cadence. The fields are now honored end-to-end. - Tests added in
tests/test_loop_intervals.pycovering field defaults, persistence, lower-bound rejection (ge=60), and end-to-end assertions that both loops invokeasyncio.sleepwith the configured value.
v2.5.0 (2026-05-06)¶
Install ID fingerprinting and optional HMAC payload signing (v2.5.0)¶
- Added — Persistent install ID. Each agent process now resolves a stable UUID per installation (default storage at
~/.guard-agent/install-id, override viaAgentConfig.install_id), sent on every request asX-Agent-Install-Id. The server uses this to detect when a single API key is being used from many distinct installs (a signal that the key has leaked or is being shared across hosts). Auto-creates the file on first call; OSError on read or write is logged vialogger.exceptionand falls through to a fresh UUID rather than failing the start-up. New module:guard_agent.install_idexposingresolve_install_id(*, state_path, override). - Added — Opt-in HMAC-SHA256 payload signing. When
AgentConfig.payload_signing_secretis set, every outbound request carriesX-Payload-Signature: v1=<hex>computed over the exact bytes that go on the wire — post-gzip and post-encryption — so the server can verify integrity againstrequest.body()without re-decoding. No header is sent when the secret is unset, preserving the existing default behavior. New module:guard_agent.signingexposingsign_payload(body, *, secret). - Added — Two new fields on
AgentConfig:install_id: str | None(override the auto-resolved install ID) andpayload_signing_secret: str | None(HMAC secret; both default toNone). - Changed — Transport sets the install-ID header once on the cached
httpx.AsyncClientdefault headers (applies to every request) and computes the signature per-request inside both encrypted and unencrypted send paths. - Tests added for both modules, full suite at 363 passed / 2 skipped.
v2.4.1 (2026-04-29)¶
Diagnostic-friendly transport error logging (v2.4.1)¶
- Fixed —
HTTPTransport._log_request_errornow formats the captured exception as<ClassName>: <repr>instead ofstr(exc). Several httpx exception classes raised on transport-level connection drops (RemoteProtocolError,WriteError, somehttpcorewrappers) carry no message body, sostr(exc)rendered empty and the previous error line wasHTTP client error for POST <url>:with no diagnostic suffix. Operators chasing a CloudFlare/origin RST storm could not tell which httpx class actually fired without attaching a debugger. The new format always shows the class identity even when the message is empty, e.g.HTTP client error for POST https://example/api/v1/events/encrypted: RemoteProtocolError: RemoteProtocolError(''). No behavior change beyond log accuracy. Coverage onguard_agent/transport.pymaintained at 100% line + 100% branch.
v2.4.0 (2026-04-29)¶
Per-event idempotency keys, configurable overflow policy, and framework-version reporting (v2.4.0)¶
Added¶
SecurityEvent.idempotency_key: UUID— every emitted event now carries a stable per-event identifier (defaultuuid4()viadefault_factory). Combined with the existing batch-stablebatch_id, this lets the SaaS dedup at the event level when an ACK is lost mid-batch and the batch is retried. The field is namedidempotency_key, notevent_id, to avoid collision with the SaaS API's existingevent_id(the prefixed external id, e.g.evt_abc123). Backward-compatible: callers that don't set the field automatically get a generated one.AgentConfig.guard_version: str | None— new optional config field set by the framework adapter (e.g. fastapi-guard middleware) at agent init time, identifying the wrapper package's version. DefaultNonefor callers that constructAgentConfigdirectly without going through a framework wrapper. Framework adapters should setconfig.guard_version = framework_package.__version__immediately before passing the config toGuardAgentHandler.EventBatch.guard_version: str | None— propagated through the wire payload on both the plaintext (/api/v1/events,/api/v1/metrics) and encrypted (/api/v1/events/encrypted) ingestion paths. Sourced fromAgentConfig.guard_version. The SaaS persists this on the project record so analytics can attribute telemetry to the wrapper version, not just the agent version. Without this field the SaaS could only seeagent_version(guard-agent's own version) and had no way to know which middleware version the customer was running.encryption._default_json_handlernow serializesUUIDvalues to their string form alongside the existingdatetime→isoformat()branch. Required for the encrypted-payload path to handle events carrying the newidempotency_key.AgentConfig.buffer_overflow_policy: Literal["drop", "block", "raise"] = "drop"— operators can now choose how the in-memory event/metric buffer behaves at capacity:drop(default) — silent eviction of the oldest entry; preserves prior behavior verbatim. Production-safe for high-throughput; loses events when the SaaS is unreachable.block— backpressures the caller until a flush frees space. Appropriate when event integrity is critical. Use only whenstart_auto_flushis wired or a flush callback is in place; otherwiseclear_bufferis the manual escape hatch.raise—BufferFullErrorpropagates to the caller. Appropriate for tests or strict environments where dropping events is unacceptable.BufferFullError(GuardAgentError)exception class added inguard_agent.exceptionsand re-exported from the top-levelguard_agentmodule.
Fixed¶
HTTPTransport._make_requestwas logging the wrong URL on POST failures. When encryption was enabled, the actual request hit/api/v1/events/encryptedbut the error log printed the unencrypted endpoint string (url = f"{endpoint}{plain_path}"). Operators chasing down 503s and decrypt errors sawPOST .../api/v1/eventsin their logs even though the wire request went to/api/v1/events/encrypted. Fix: compute the actual posted URL (encrypted vs plain) up-front and pass that to_log_request_error. No behavior change beyond log accuracy.
Compatibility¶
- Default behavior unchanged for callers that don't opt into either feature:
idempotency_keyhas adefault_factory, andbuffer_overflow_policydefaults to"drop"(which preserves prior eviction semantics including the silent-overflow counter and warning-every-100th log). - SaaS-side coordination: this release is paired with the SaaS dedup work that ships the
idempotency_keycolumn onsecurity_events, the unique constraint, and thepg_insert ... on_conflict_do_nothingingest path. SaaS deployments that don't yet recognize the field treat it as an unknown column and silently drop the bytes — no behavior change to those callers.
v2.3.0 (2026-04-26)¶
Production Safety (v2.3.0)¶
- Fork-safe
GuardAgentHandlersingleton. Class-level_instancesurvivedos.fork(), so Gunicorn pre-fork workers all inherited a stale_initialized=Trueflag and dead asyncio task handles from the parent loop. Callingstart()in the child was a silent no-op and the child never connected to the agent endpoint. Register anos.register_at_fork(after_in_child=...)hook that resets_initializedand clears inherited task references. Add a per-call PID guard for non-fork-aware multiprocessing setups. Companion fix to the transport-level fork-safety shipped in 2.2.0. - Real watermark-driven early flush.
EventBuffer._flush_if_neededwas previously a no-op marker — bursts that filled the buffer continued dropping events for the entire flush_interval window even though the early-flush task was scheduled. Trigger a real async flush when buffer occupancy exceeds the high-watermark ratio (default 80%). Cap concurrent flushes via anasyncio.Semaphore(default 1) to prevent runaway parallel sends under sustained pressure. NewAgentConfigfields:high_watermark_ratio: float = 0.8,max_concurrent_flushes: int = 1.EventBuffer.stop_auto_flush()now awaits in-flight watermark-triggered flushes before returning, eliminating data loss on shutdown. - Hard-fail on encryption init. When the
project_encryption_keyround-trip failed at startup, transport logged a warning and proceeded with plaintext over the wire. Operators got no signal stronger than a log line and could ship traffic encrypted in the dashboard's mind but not on the network. Now raisesEncryptionConfigErroron any startup encryption failure. No plaintext fallback. Operators get a loud failure they can react to.
Internal (v2.3.0)¶
- Test coverage maintained at 100% line + branch across
guard_agent/buffer.py,client.py,encryption.py,models.py,protocols.py,transport.py,utils.py(1135 statements, 0 missed). - Added test coverage to verify
EventBatch.batch_idis stable across retries (the underlying behavior was already correct in 2.2.0 — the new tests pin it down so future refactors can't regress). - Performance tests (
test_agent_performance_impact,test_memory_usage) hardened against coverage-instrumentation noise:gc.collect()before RSS baseline, coverage-aware overhead threshold,enable_redis=Falsein test apps to eliminate ResourceWarning pollution. - Fixed pre-existing typing gaps in test mocks; all resolved at the root without any suppression directives.
v2.2.0 (2026-04-25)¶
Production Safety (v2.2.0)¶
- Fork-safe transport.
HTTPTransport.__init__registers anos.register_at_fork(after_in_child=...)hook that resets the inheritedhttpx.AsyncClient,CircuitBreaker, andRateLimiterin every forked child. A pid-drift check runs on every send so spawn-style workers (uvicorn--workerswithout--preload) get the same protection. Fixes a class of bugs where Gunicorn--preloadworkers would corrupt the shared socket between parent and child. - Observable buffer drops.
EventBuffernow exposesevents_droppedandmetrics_droppedcounters viaget_stats(). The first drop and every 100th drop log aWARN. Previously the deque silently evicted the oldest event when full. - Honor server
Retry-After. A 429 response now raisesRateLimitedError(retry_after_seconds=...), and_send_with_retry/_get_with_retrysleep that exact value (capped at 300s) instead of falling back to client-side exponential backoff. Prevents the agent from hammering an already-overloaded SaaS. - Persist-confirm Redis recovery. Redis persist keys are now
event_{ns}_{uuid8}/metric_{ns}_{uuid8}so two events arriving in the same millisecond no longer collide. Deletion happens only after the transport confirms via the newconfirm_event_redis_keys/confirm_metric_redis_keyshelpers, andrequeue_events_in_memory/requeue_metrics_in_memorypush unsent events back to the front of the buffer on transport failure. Previously a transport failure cleared both deque and Redis simultaneously, dropping the events permanently. BufferProtocoladds new methods.flush_events_with_keys,flush_metrics_with_keys,confirm_event_redis_keys,confirm_metric_redis_keys,requeue_events_in_memory,requeue_metrics_in_memory. Custom buffer implementations need to implement these or fall back to the bundledEventBuffer.
Compression (v2.2.0)¶
- Gzip compression of outgoing batch bodies above
compression_threshold(default 1024 bytes). When the body exceeds the threshold the agent compresses with gzip and sendsContent-Encoding: gzip; the Guard Core SaaS decompresses request bodies via itsGzipRequestMiddlewarebefore pydantic validation. Smaller bodies skip compression and ship as plain JSON. - Default is ON.
AgentConfig.compression_enabled=True. Setcompression_enabled=Falseif you are pointing the agent at an ingestion endpoint that does not handleContent-Encoding: gziprequest bodies (e.g. a custom backend without a decompression middleware). EventBatch.compressedfield now reflects whether the body was actually compressed.
Versioning hygiene (v2.2.0)¶
agent_versionin HTTP request headers (User-Agent) and batch payloads now derives fromguard_agent.__version__instead of the hardcoded"1.1.0"string the previous releases were sending. SaaS-side analytics that key offagent_versionwill now see the real installed version.
Test coverage (v2.2.0)¶
- Test coverage raised to 100% across
guard_agent/buffer.py,client.py,encryption.py,models.py,protocols.py,transport.py,utils.py(1053 statements, 0 missed). Adds the previously-missing branches for the fork-hook unavailable path, the Retry-After exhausted-attempts path, the GET retry-after path, the buffer overflow drop accounting, the empty-key forget paths, the Redis-failure swallowing inconfirm_event_redis_keys/confirm_metric_redis_keys, and therequeue_metrics_in_memoryend-to-end + overflow-drop paths.
v2.1.0 (2026-04-24)¶
Multi-Adapter Coverage (v2.1.0)¶
- Added per-adapter integration smoke tests:
tests/test_adapter_fastapi.py,test_adapter_flask.py,test_adapter_django.py. Each verifiesSecurityConfig.to_agent_config()roundtrip and request delivery through the adapter's middleware withenable_agent=True. - Added per-adapter documentation pages under
docs/adapters/: FastAPI, Flask, Django, Tornado. Each page covers install, minimal example, and agent wiring specific to that framework. mkdocs.ymlnavigation updated with a new top-level Adapters section.
Dependency Changes (v2.1.0)¶
- Added
django,djapi-guard>=2.0.0,flask,flaskapi-guard>=2.0.0,tornadoto[project.optional-dependencies].devso the test suite can exercise every adapter. tornadoapi-guardis not yet included in dev extras — it has not been published to PyPI (only a yanked 0.0.1 exists). Integration tests for Tornado are stubbed withpytest.mark.skipintests/test_adapter_tornado.py. Re-enable once the adapter ships a 1.0.0+ release.
v2.0.0 (2026-04-24)¶
Package Rename (v2.0.0)¶
- Renamed on PyPI:
fastapi-guard-agent→guard-agent. The Python import path (from guard_agent import ...) is unchanged — no code changes are required in consuming applications. - Repositioned as a framework-agnostic telemetry agent serving
fastapi-guard,flaskapi-guard,djapi-guard, andtornadoapi-guard. - Legacy name preserved: a meta-package
fastapi-guard-agent==1.2.0is published alongside this release, whose only dependency isguard-agent>=2.0.0,<3.0.0. Existingpip install fastapi-guard-agentinvocations continue to resolve correctly and pull the renamed distribution transitively. - Repository renamed on GitHub:
rennf93/fastapi-guard-agent→rennf93/guard-agent. GitHub auto-redirects the old URLs. - Documentation site moved to
https://rennf93.github.io/guard-agent/.
Dependency Changes (v2.0.0)¶
- Removed
fastapiandfastapi-guardfrom runtime dependencies — the agent is framework-agnostic and speaks HTTP to the dashboard, not to any web framework. - Runtime deps are now:
cryptography,httpx,pydantic,typing-extensions. fastapiandfastapi-guardremain as dev extras so the existing test suite keeps passing. Each framework adapter brings its own web framework.- Dropped
Framework :: FastAPIclassifier; development status promoted fromAlphatoBeta.
Breaking Changes (v2.0.0)¶
- None in Python API —
from guard_agent import ...,GuardAgentHandler,AgentConfig, and every public symbol behave identically. - Distribution name change only: scripts, Dockerfiles, and lockfiles that install
fastapi-guard-agentdirectly should migrate toguard-agent. The shim keeps old commands working but new projects should installguard-agentdirectly.
Migration Guide (v2.0.0)¶
- Existing code: no changes.
- Install commands (uv): replace
uv add fastapi-guard-agentwithuv add guard-agentat your leisure — both resolve to the same underlying package. - Poetry / pip equivalents:
poetry add guard-agent/pip install guard-agent. - Lockfiles: running
uv lock,poetry lock, orpip-compileafter bumping will transparently update entries toguard-agent.
v1.1.1 (2026-03-11)¶
Bug Fixes (v1.1.1)¶
- Fixed misalignment on documentation headers and model parameters.
- Added support for Python 3.14.
Maintenance (v1.1.1)¶
- Code alignment and cleanup.
v1.1.0 (2025-10-14)¶
New Features (v1.1.0)¶
- Added end-to-end payload encryption for secure telemetry transmission using AES-256-GCM.
- Implemented
PayloadEncryptorclass with project-specific encryption keys. - Added encrypted endpoint support for events and metrics (
/api/v1/events/encrypted). - Integrated automatic datetime serialization in encrypted payloads via custom JSON handler.
- Added encryption key verification during transport initialization.
Technical Details (v1.1.0)¶
- Encryption uses AES-256-GCM with 96-bit nonces and 128-bit authentication tags.
- Pydantic models are serialized using
.model_dump(mode="json")before encryption. - Custom
_default_json_handlerensures datetime objects are properly ISO-formatted.
v1.0.2 (2025-09-12)¶
Enhancements (v1.0.2)¶
- Added dynamic rule updated event type.
v1.0.1 (2025-08-07)¶
Enhancements (v1.0.1)¶
- Added path_excluded event type.
v1.0.0 (2025-07-24)¶
Official Release¶
v0.1.1 (2025-07-09)¶
Enhancements (v0.1.1)¶
- Standardized Redis Protocl/Manager methods across libraries.
v0.1.0 (2025-07-08)¶
Enhancements (v0.1.0)¶
- Switched from aiohttp to httpx for HTTP client.
- Completed implementation.
- 100% test coverage.
v0.0.1 (2025-06-22)¶
New Features (v0.0.1)¶
- Initial release FastAPI Guard Agent.