Skip to content

Commit 95392da

Browse files
committed
Release: honor dynamic_rule_interval and add agent_status_interval
Surface configuration for two agent loop intervals that were previously shadowed by hardcoded 300-second values in guard-agent's _rules_loop and _status_loop. Both changes documented under the v3.1.0 CHANGELOG entry. Fixed: - SecurityConfig.dynamic_rule_interval is now actually honored. to_agent_config() previously dropped the field on the floor, so guard-agent's _rules_loop ran on a hardcoded 300s regardless of user configuration. The value is now forwarded through to AgentConfig.dynamic_rule_interval. Effective with guard-agent >= 2.6.0 (matching field added there). Added: - SecurityConfig.agent_status_interval - new int field (default 300, range 60-86400) controlling how often the agent reports its status to the SaaS dashboard. Forwarded to AgentConfig.status_interval. Pairs with guard-agent >= 2.6.0 which actually honors the value. Tests: - Five new cases in tests/test_agent/test_models_agent_integration.py covering forwarding of both interval fields, default value, lower-bound rejection (60), and upper-bound rejection (86400).
1 parent 695bd04 commit 95392da

4 files changed

Lines changed: 77 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Production reliability + ergonomics: NOSCRIPT recovery, lazy_init by default, cl
2424
- **Cloud-IP store class-as-factory resolution.** `HandlerInitializer._resolve_cloud_ip_store` now treats a bare class object (e.g. `cloud_ip_store=RedisCloudIpStore`) as a factory and invokes it with `redis_handler`, instead of mistaking it for an instance via `isinstance(cls, Protocol)` (which returns `True` for runtime-checkable protocol classes).
2525
- **Lazy-init partial-failure isolation.** `_run_lazy_init` previously wrapped both cloud-IP and geo-IP initialization in a single `try` — a cloud failure permanently disabled geo init. Each is now wrapped independently so a transient cloud-API outage no longer blocks geo lookups. With `lazy_init=True` now the default, this prevents silent loss of geo enforcement.
2626
- **PR #19 fallout cleanup.** Cleared 14 ruff F821/UP037 errors and 5 mypy errors that PR #19 left behind: missing `Literal` imports in both decorator base files, narrow-type assignment in `dynamic_rule_handler`, and invariant-`set` argument-type mismatches at `cloud_handler.initialize_redis` call sites in both async and sync `handler_initializer`.
27+
- **`SecurityConfig.dynamic_rule_interval` is now actually honored.** `to_agent_config()` previously dropped this field on the floor; the agent's `_rules_loop` ran on a hardcoded 300s regardless of what users configured. Fixed by forwarding the value through to `AgentConfig.dynamic_rule_interval`. Effective once `guard-agent >= 2.6.0` is installed (the agent side adds the matching field).
2728

2829
### Changed
2930

@@ -36,6 +37,7 @@ Production reliability + ergonomics: NOSCRIPT recovery, lazy_init by default, cl
3637
- **`cloud_ip_store` accepts a `CloudIpStoreFactory` callable** (`Callable[[RedisHandlerProtocol], CloudIpStoreProtocol]`), letting users defer store construction until the Redis handler is built. Eliminates the chicken-and-egg pattern of constructing a throwaway `RedisManager` just to feed `RedisCloudIpStore`. Sync protocol mirror exposes `SyncCloudIpStoreFactory`.
3738
- **`CloudProvider` Literal alias and `VALID_CLOUD_PROVIDERS` frozenset** exported from `guard_core.models`. Type alias with single source of truth: `CloudProvider = Literal["AWS", "GCP", "Azure"]`; runtime guard set: `VALID_CLOUD_PROVIDERS = frozenset(get_args(CloudProvider))`.
3839
- **`rate_limit_script_reloaded` SecurityEvent** emitted on NOSCRIPT recovery so dashboards can detect repeated reloads (signal of unstable Redis).
40+
- **`SecurityConfig.agent_status_interval`** — new `int` field (default 300, range 60-86400) controlling how often the agent reports its status to the SaaS dashboard. Forwarded to `AgentConfig.status_interval`. Pairs with `guard-agent >= 2.6.0` which actually honors the value (the agent previously hardcoded 300).
3941

4042
___
4143

docs/release-notes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Production reliability + ergonomics: NOSCRIPT recovery, lazy_init by default, cl
2929
- **Cloud-IP store class-as-factory resolution.** `HandlerInitializer` now treats a bare class object passed via `cloud_ip_store=RedisCloudIpStore` as a factory and invokes it with `redis_handler`.
3030
- **Lazy-init partial-failure isolation.** `_run_lazy_init` wraps cloud-IP and geo-IP initialization in independent `try` blocks so a cloud failure no longer disables geo init. Important now that `lazy_init=True` is the default.
3131
- **PR #19 fallout cleanup.** Cleared 14 ruff F821/UP037 errors and 5 mypy errors left behind by PR #19.
32+
- **`SecurityConfig.dynamic_rule_interval` is now actually honored.** `to_agent_config()` previously dropped this field on the floor; the agent's `_rules_loop` ran on a hardcoded 300s regardless of what users configured. Fixed by forwarding the value through to `AgentConfig.dynamic_rule_interval`. Effective once `guard-agent >= 2.6.0` is installed.
3233

3334
### Changed
3435

@@ -41,6 +42,7 @@ Production reliability + ergonomics: NOSCRIPT recovery, lazy_init by default, cl
4142
- **`cloud_ip_store` accepts a `CloudIpStoreFactory` callable** (`Callable[[RedisHandlerProtocol], CloudIpStoreProtocol]`). Sync mirror exposes `SyncCloudIpStoreFactory`.
4243
- **`CloudProvider` Literal alias and `VALID_CLOUD_PROVIDERS` frozenset** exported from `guard_core.models`.
4344
- **`rate_limit_script_reloaded` SecurityEvent** emitted on NOSCRIPT recovery.
45+
- **`SecurityConfig.agent_status_interval`** — new `int` field (default 300, range 60-86400) controlling how often the agent reports its status to the SaaS dashboard. Forwarded to `AgentConfig.status_interval`. Pairs with `guard-agent >= 2.6.0` which actually honors the value (the agent previously hardcoded 300).
4446

4547
___
4648

guard_core/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,13 @@ class SecurityConfig(BaseModel):
376376
default=300, description="Interval in seconds between dynamic rule updates"
377377
)
378378

379+
agent_status_interval: int = Field(
380+
default=300,
381+
ge=60,
382+
le=86400,
383+
description="Interval in seconds between agent status reports to the SaaS",
384+
)
385+
379386
emergency_mode: bool = Field(
380387
default=False, description="Emergency lockdown mode (set by dynamic rules)"
381388
)
@@ -697,6 +704,8 @@ def to_agent_config(self) -> "AgentConfig | None":
697704
project_id=self.agent_project_id,
698705
buffer_size=self.agent_buffer_size,
699706
flush_interval=self.agent_flush_interval,
707+
dynamic_rule_interval=self.dynamic_rule_interval,
708+
status_interval=self.agent_status_interval,
700709
enable_events=self.agent_enable_events,
701710
enable_metrics=self.agent_enable_metrics,
702711
timeout=self.agent_timeout,

tests/test_agent/test_models_agent_integration.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,67 @@ def test_valid_agent_and_dynamic_rules_config() -> None:
177177
assert config.enable_agent is True
178178
assert config.enable_dynamic_rules is True
179179
assert config.dynamic_rule_interval == 600
180+
181+
182+
def test_to_agent_config_forwards_dynamic_rule_interval() -> None:
183+
"""`dynamic_rule_interval` was added to AgentConfig in guard-agent 2.6.0.
184+
185+
Asserted at the call-site contract level — to_agent_config() must pass
186+
dynamic_rule_interval as a kwarg to AgentConfig. Older installed agents
187+
silently drop unknown kwargs.
188+
"""
189+
import guard_agent
190+
191+
config = SecurityConfig(
192+
enable_agent=True,
193+
agent_api_key="test-key",
194+
dynamic_rule_interval=600,
195+
)
196+
197+
spy = MagicMock(name="AgentConfig")
198+
with patch.object(guard_agent, "AgentConfig", spy):
199+
config.to_agent_config()
200+
201+
spy.assert_called_once()
202+
assert spy.call_args.kwargs.get("dynamic_rule_interval") == 600
203+
204+
205+
def test_to_agent_config_forwards_agent_status_interval() -> None:
206+
"""`status_interval` was added to AgentConfig in guard-agent 2.6.0.
207+
208+
Asserted at the call-site contract level — to_agent_config() must pass
209+
status_interval as a kwarg to AgentConfig.
210+
"""
211+
import guard_agent
212+
213+
config = SecurityConfig(
214+
enable_agent=True,
215+
agent_api_key="test-key",
216+
agent_status_interval=900,
217+
)
218+
219+
spy = MagicMock(name="AgentConfig")
220+
with patch.object(guard_agent, "AgentConfig", spy):
221+
config.to_agent_config()
222+
223+
spy.assert_called_once()
224+
assert spy.call_args.kwargs.get("status_interval") == 900
225+
226+
227+
def test_agent_status_interval_defaults_to_300() -> None:
228+
config = SecurityConfig()
229+
assert config.agent_status_interval == 300
230+
231+
232+
def test_agent_status_interval_rejects_below_60() -> None:
233+
from pydantic import ValidationError
234+
235+
with pytest.raises(ValidationError):
236+
SecurityConfig(agent_status_interval=30)
237+
238+
239+
def test_agent_status_interval_rejects_above_86400() -> None:
240+
from pydantic import ValidationError
241+
242+
with pytest.raises(ValidationError):
243+
SecurityConfig(agent_status_interval=86401)

0 commit comments

Comments
 (0)