diff --git a/hive/async_stack.py b/hive/async_stack.py index 7d5f120..ba28ed3 100644 --- a/hive/async_stack.py +++ b/hive/async_stack.py @@ -96,7 +96,8 @@ async def remember( async with self._lock: loop = asyncio.get_running_loop() return await loop.run_in_executor( - None, self._stack.remember, key, value, + None, + lambda: self._stack.remember(key, value, trust=trust, tags=tags), ) async def recall(self, key: str, default: Any = None) -> Any: diff --git a/hive/config.py b/hive/config.py index cdceeb1..3d33786 100644 --- a/hive/config.py +++ b/hive/config.py @@ -29,6 +29,7 @@ class HiveConfig: rate_limit: int = 0 # 0 = disabled default_ttl_s: float | None = None max_memory_nodes: int = 10_000 + max_content_bytes: int = 1_048_576 jwt_secret: str | None = None otel_endpoint: str | None = None prometheus_port: int = 0 # 0 = disabled @@ -68,6 +69,12 @@ def _parse_bool(raw: str) -> bool: else: kwargs[attr] = raw + # K8s/Helm manifests use HIVE_MAX_NODES (not HIVE_MAX_MEMORY_NODES). + if "max_memory_nodes" not in kwargs: + alias = os.environ.get(f"{prefix}MAX_NODES") + if alias is not None: + kwargs["max_memory_nodes"] = int(alias) + return cls(**kwargs) def validate(self) -> None: @@ -76,6 +83,8 @@ def validate(self) -> None: raise ValueError("rate_limit must be >= 0") if self.max_memory_nodes < 1: raise ValueError("max_memory_nodes must be >= 1") + if self.max_content_bytes < 1: + raise ValueError("max_content_bytes must be >= 1") def to_dict(self) -> dict[str, Any]: """Return a plain dict (useful for JSON logging).""" diff --git a/hive/stack.py b/hive/stack.py index 471903d..bef8504 100644 --- a/hive/stack.py +++ b/hive/stack.py @@ -147,7 +147,7 @@ def __init__( validate: bool = False, config: HiveConfig | None = None, rate_limiter: RateLimiter | None = None, - max_content_bytes: int = 1_048_576, + max_content_bytes: int | None = None, circuit_breaker: CircuitBreaker | None = None, ) -> None: self.config = config or HiveConfig() @@ -163,12 +163,22 @@ def __init__( tenant_id=tenant_id, tenant_isolation=self.config.tenant_isolation, default_ttl_s=self.config.default_ttl_s, + max_nodes=self.config.max_memory_nodes, ) self._tenant_id = tenant_id self._validate = validate or self.config.validate_inputs self.rate_limiter = rate_limiter + if self.rate_limiter is None and self.config.rate_limit > 0: + self.rate_limiter = RateLimiter( + default_capacity=self.config.rate_limit, + refill_rate=max(self.config.rate_limit / 60.0, 1.0), + ) self.circuit_breaker = circuit_breaker - self._max_content_bytes = max_content_bytes + self._max_content_bytes = ( + max_content_bytes + if max_content_bytes is not None + else self.config.max_content_bytes + ) self.telemetry = telemetry self.feedback = feedback_buffer self._policy_updater = PolicyUpdater() if feedback_buffer is not None else None diff --git a/scripts/hive_api_server.py b/scripts/hive_api_server.py index 481c389..1ef7ba8 100644 --- a/scripts/hive_api_server.py +++ b/scripts/hive_api_server.py @@ -23,6 +23,7 @@ from typing import Any from hive import HiveStack +from hive.config import HiveConfig from hive.rule_fast import RuleFastHoneyComb @@ -39,7 +40,7 @@ if _HAS_FASTAPI: app = FastAPI(title="Hive Agent Memory", version="0.5.0") - stack = HiveStack(honey_comb=RuleFastHoneyComb()) + stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=HiveConfig.from_env()) class RouteRequest(BaseModel): goal: str = Field(default="") diff --git a/tests/test_async_stack.py b/tests/test_async_stack.py index 2c6a54e..543b96b 100644 --- a/tests/test_async_stack.py +++ b/tests/test_async_stack.py @@ -30,6 +30,16 @@ async def test_async_remember_recall(): assert await stack.recall("k") == "v" +@pytest.mark.asyncio +async def test_async_remember_passes_trust_and_tags(): + stack = AsyncHiveStack() + await stack.remember("k", "v", trust=0.25, tags={"audit"}) + node = stack.stack.brain.get("k") + assert node is not None + assert node.trust == 0.25 + assert node.tags == {"audit"} + + @pytest.mark.asyncio async def test_async_compress_many(): stack = AsyncHiveStack(honey_comb=RuleFastHoneyComb()) diff --git a/tests/test_enterprise_config.py b/tests/test_enterprise_config.py index c243b6f..27e165c 100644 --- a/tests/test_enterprise_config.py +++ b/tests/test_enterprise_config.py @@ -54,3 +54,37 @@ def test_config_to_dict(): d = cfg.to_dict() assert d["rate_limit"] == 100 assert "validate_inputs" in d + + +def test_from_env_reads_max_nodes_alias(): + os.environ["HIVE_MAX_NODES"] = "42" + try: + cfg = HiveConfig.from_env() + assert cfg.max_memory_nodes == 42 + finally: + del os.environ["HIVE_MAX_NODES"] + + +def test_from_env_reads_max_content_bytes(): + os.environ["HIVE_MAX_CONTENT_BYTES"] = "2048" + try: + cfg = HiveConfig.from_env() + assert cfg.max_content_bytes == 2048 + finally: + del os.environ["HIVE_MAX_CONTENT_BYTES"] + + +def test_stack_applies_config_memory_and_content_limits(): + cfg = HiveConfig(max_memory_nodes=3, max_content_bytes=64) + stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=cfg) + assert stack.brain._max_nodes == 3 + assert stack._max_content_bytes == 64 + + +def test_stack_auto_rate_limiter_from_config(): + cfg = HiveConfig(rate_limit=1) + stack = HiveStack(honey_comb=RuleFastHoneyComb(), config=cfg) + assert stack.rate_limiter is not None + stack.route({"goal": "first", "available_tools": []}) + limited = stack.route({"goal": "second", "available_tools": []}) + assert limited.source == "ratelimit" diff --git a/tests/test_hive_api_server.py b/tests/test_hive_api_server.py index f25d0f2..8120592 100644 --- a/tests/test_hive_api_server.py +++ b/tests/test_hive_api_server.py @@ -41,3 +41,29 @@ def test_ready_returns_200_when_stack_is_healthy(): resp = client.get("/ready") assert resp.status_code == 200 assert resp.json()["status"] == "ready" + + +@pytest.mark.skipif(not _HAS_FASTAPI, reason="fastapi not installed") +def test_api_server_applies_validate_inputs_from_env(monkeypatch): + """K8s sets HIVE_VALIDATE_INPUTS=true; the REST server must honor it.""" + from hive import HiveStack + from hive.config import HiveConfig + from hive.rule_fast import RuleFastHoneyComb + + monkeypatch.setenv("HIVE_VALIDATE_INPUTS", "true") + api_server.stack = HiveStack( + honey_comb=RuleFastHoneyComb(), + config=HiveConfig.from_env(), + ) + client = TestClient(api_server.app, raise_server_exceptions=False) + # RouteRequest allows any goal length; AgentState caps at 4096 when validating. + resp = client.post( + "/route", + json={"goal": "x" * 5000, "step": 0, "available_tools": []}, + ) + assert resp.status_code == 500 + monkeypatch.delenv("HIVE_VALIDATE_INPUTS", raising=False) + api_server.stack = HiveStack( + honey_comb=RuleFastHoneyComb(), + config=HiveConfig.from_env(), + )