Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agentkit/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ def get_agent(
max_steps=settings.MAX_STEPS,
run_timeout_s=settings.RUN_TIMEOUT_S,
tool_timeout_s=settings.TOOL_TIMEOUT_S,
tool_output_max_chars=settings.TOOL_OUTPUT_MAX_CHARS,
)
return Agent(
llm=llm,
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ services:
- LLM_MODEL=${LLM_MODEL:-gemini-3.1-flash-lite}
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- SEARCH_API_KEY=${SEARCH_API_KEY:-}
- RATE_LIMIT_PER_MIN=${RATE_LIMIT_PER_MIN:-30}
- MAX_STEPS=${MAX_STEPS:-10}
- TOOL_TIMEOUT_S=${TOOL_TIMEOUT_S:-15}
- SESSION_TTL_S=${SESSION_TTL_S:-3600}
- TOOL_OUTPUT_MAX_CHARS=${TOOL_OUTPUT_MAX_CHARS:-2000}
- LLM_MAX_RETRIES=${LLM_MAX_RETRIES:-3}
- LLM_RETRY_BASE_DELAY_S=${LLM_RETRY_BASE_DELAY_S:-0.5}
- LLM_RETRY_MAX_DELAY_S=${LLM_RETRY_MAX_DELAY_S:-10}
volumes:
- sandbox_data:/tmp/agentkit_sandbox
healthcheck:
Expand Down
2 changes: 1 addition & 1 deletion docs/AGENTKIT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ Add an index on `steps(run_id, step_no)` and `runs(session_id)`. Manage schema c

All configuration comes from environment variables, loaded through a Pydantic `BaseSettings` class. Provide a `.env.example` file. Never commit secrets.

Required or supported variables include: `DATABASE_URL`, `REDIS_URL`, `LLM_PROVIDER`, `LLM_MODEL`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, `SEARCH_API_KEY`, `API_KEYS` (comma-separated), `MAX_STEPS`, `RUN_TIMEOUT_S`, `TOOL_TIMEOUT_S`, `RATE_LIMIT_PER_MIN`, `FILE_TOOL_BASE_DIR`, `SESSION_TTL_S`, `TOOL_OUTPUT_MAX_CHARS`.
Required or supported variables include: `DATABASE_URL`, `REDIS_URL`, `LLM_PROVIDER`, `LLM_MODEL`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, `SEARCH_API_KEY`, `LLM_MAX_RETRIES`, `LLM_RETRY_BASE_DELAY_S`, `LLM_RETRY_MAX_DELAY_S`, `FILE_TOOL_BASE_DIR`, `API_KEYS` (comma-separated), `RATE_LIMIT_PER_MIN`, `MAX_STEPS`, `RUN_TIMEOUT_S`, `TOOL_TIMEOUT_S`, `SESSION_TTL_S`, `TOOL_OUTPUT_MAX_CHARS`.

## 9. Repository layout

Expand Down
8 changes: 8 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ This document tracks execution progress across all backlog tasks, including pull
| AUD-09 | AUD | Live End-to-End Infrastructure Verification & Documentation Proof | `docs/e2e-verification-report` | #53 | 3 | Done | Standardized model, verified live stack, and documented in E2E_REPORT.md |
| AUD-10 | AUD | Rate Limiter Outage Fallback & Provider-Valid Session History | `fix/ratelimit-and-session-repair` | #56 | 3 | Done | In-memory fallback on Redis connection failures; synthetic tool results repair aborted sessions |
| M13-T06 | M13 | Whole-Project Audit Remediation | `fix/audit-hardening` | #47 | 3 | Done | Fail-closed config and LLM resolution, SSRF post-connect check, atomic fallback |
| AUDIT-12 | AUD | Wire TOOL_OUTPUT_MAX_CHARS and Documented Compose Settings | `fix/wire-documented-settings` | #82 | 1 | Done | Wire tool_output_max_chars in get_agent, expose missing settings in docker-compose.yml |

---

Expand Down Expand Up @@ -407,3 +408,10 @@ This document tracks execution progress across all backlog tasks, including pull
- **PR**: #47
- **Commits**: 3
- **Notes**: Remediated 17 findings from a full-codebase audit, rebased onto the AUD-01/AUD-04/AUD-05 follow-ups. Critical: removed the `ak_test_key_12345` default from `Settings.API_KEYS` and made the field required; replaced the `FakeLLMClient` fallback in `get_llm_client` with a `ServiceUnavailableError` (503) boundary around provider construction; kept the AUD-04 factory-level `RetryingLLMClient` wrapping and made it configurable through new `LLM_MAX_RETRIES`/`LLM_RETRY_BASE_DELAY_S`/`LLM_RETRY_MAX_DELAY_S` settings; made the per-request `Agent` assembly's override explicit as `app.state.agent_override`; forwarded `get_run_trace`/`list_runs` through `TieredMemoryStore`; moved `http_fetch` DNS off the event loop with `asyncio.to_thread`, added a post-connect `server_addr` check against the validated addresses to close the DNS-rebinding TOCTOU window, and replaced full-body buffering with a byte-budgeted stream; replaced the rate limiter's non-scripting fallback with `WATCH`/`MULTI`/`EXEC`, leaving the AUD-10 in-memory fallback for Redis outages intact. Moderate: corrected the `RedisMemoryStore.truncate_messages` off-by-one that returned `max_messages + 1` entries when a system prompt was present; switched generated `tool_call_id`s to full UUIDs; wrapped response parsing in both LLM adapters inside the error boundary so malformed responses and malformed tool-argument JSON raise `LLMProviderError`; guarded `RetryingLLMClient.__getattr__` against recursion on a partially constructed instance; sanitized `/health` details so the unauthenticated endpoint no longer echoes driver error strings; made `sql_readonly` mask string literals and quoted identifiers before its keyword scan, allow `#>`/`#>>`/`#-`, apply the row limit in SQL and push `statement_timeout` to PostgreSQL. Minor: stopped logging raw run goals at INFO (length only, content at DEBUG); replaced `split(";")` in `scripts/seed.py` with a statement-aware splitter handling literals, comments and dollar-quotes; bounded calculator exponentiation by result bit length. Added `tests/unit/test_audit_regressions.py` and a real-Redis concurrency regression test pinning the fallback fix. Recorded in ADR-009, ADR-010, ADR-011. Suite: 298 passed, 25 skipped; coverage 92.86% on core/tools/llm; `ruff check`, `ruff format --check` and `mypy agentkit tests` all clean.


### AUDIT-12: Wire TOOL_OUTPUT_MAX_CHARS and Documented Compose Settings
- **Status**: Done
- **PR**: #82
- **Commits**: 1
- **Notes**: In `agentkit/api/deps.py`, wired `settings.TOOL_OUTPUT_MAX_CHARS` into `AgentConfig(tool_output_max_chars=...)` within `get_agent()`. In `docker-compose.yml`, exposed all documented settings (`SEARCH_API_KEY`, `RATE_LIMIT_PER_MIN`, `MAX_STEPS`, `TOOL_TIMEOUT_S`, `SESSION_TTL_S`, `TOOL_OUTPUT_MAX_CHARS`, `LLM_MAX_RETRIES`, `LLM_RETRY_BASE_DELAY_S`, `LLM_RETRY_MAX_DELAY_S`) under `services.api.environment`. Aligned `docs/SPEC.md` and `docs/AGENTKIT_SPEC.md` with the full configuration set. Added unit tests in `tests/unit/test_config.py` verifying dependency wiring, tool truncation behavior, and full compose environment parity.
2 changes: 1 addition & 1 deletion docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ Add an index on `steps(run_id, step_no)` and `runs(session_id)`. Manage schema c

All configuration comes from environment variables, loaded through a Pydantic `BaseSettings` class. Provide a `.env.example` file. Never commit secrets.

Required or supported variables include: `DATABASE_URL`, `REDIS_URL`, `LLM_PROVIDER`, `LLM_MODEL`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, `SEARCH_API_KEY`, `API_KEYS` (comma-separated), `MAX_STEPS`, `RUN_TIMEOUT_S`, `TOOL_TIMEOUT_S`, `RATE_LIMIT_PER_MIN`, `FILE_TOOL_BASE_DIR`, `SESSION_TTL_S`, `TOOL_OUTPUT_MAX_CHARS`.
Required or supported variables include: `DATABASE_URL`, `REDIS_URL`, `LLM_PROVIDER`, `LLM_MODEL`, `GEMINI_API_KEY`, `OPENAI_API_KEY`, `SEARCH_API_KEY`, `LLM_MAX_RETRIES`, `LLM_RETRY_BASE_DELAY_S`, `LLM_RETRY_MAX_DELAY_S`, `FILE_TOOL_BASE_DIR`, `API_KEYS` (comma-separated), `RATE_LIMIT_PER_MIN`, `MAX_STEPS`, `RUN_TIMEOUT_S`, `TOOL_TIMEOUT_S`, `SESSION_TTL_S`, `TOOL_OUTPUT_MAX_CHARS`.

## 9. Repository layout

Expand Down
112 changes: 112 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
"""Unit tests for configuration management via Pydantic BaseSettings."""

from pathlib import Path
from types import SimpleNamespace

import pytest
import yaml
from pydantic import ValidationError

from agentkit.api.deps import get_agent
from agentkit.config import Settings
from agentkit.core.trace import InMemoryTraceSink
from agentkit.llm.base import LLMResponse
from agentkit.llm.fake import FakeLLMClient
from agentkit.memory.base import InMemoryMemoryStore
from agentkit.tools.models import ToolCall
from agentkit.tools.registry import ToolRegistry


def test_default_settings() -> None:
Expand Down Expand Up @@ -60,3 +71,104 @@ def test_non_positive_steps_raises_validation_error() -> None:
API_KEYS="key",
MAX_STEPS=0,
)


def test_get_agent_wires_tool_output_max_chars() -> None:
"""AUDIT-12: get_agent wires Settings.TOOL_OUTPUT_MAX_CHARS to AgentConfig."""
settings = Settings(
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/agentkit",
REDIS_URL="redis://localhost:6379/0",
API_KEYS="secret_key",
TOOL_OUTPUT_MAX_CHARS=50,
)

class _DummyApp:
state = SimpleNamespace(agent_override=None)

request = SimpleNamespace(app=_DummyApp())

agent = get_agent(
request=request, # type: ignore[arg-type]
settings=settings,
llm=FakeLLMClient(),
registry=ToolRegistry(),
memory=InMemoryMemoryStore(),
trace=InMemoryTraceSink(),
)
assert agent.config.tool_output_max_chars == 50


@pytest.mark.asyncio
async def test_tool_output_truncation_with_configured_max_chars() -> None:
"""AUDIT-12: Tool outputs exceeding configured max_chars are truncated."""
settings = Settings(
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/agentkit",
REDIS_URL="redis://localhost:6379/0",
API_KEYS="secret_key",
TOOL_OUTPUT_MAX_CHARS=40,
)

registry = ToolRegistry()

def long_output_tool() -> str:
return "A" * 120

registry.register(long_output_tool, name="long_tool", description="Returns long string.")

fake_llm = FakeLLMClient(
responses=[
LLMResponse(
tool_calls=[ToolCall(id="call_long", name="long_tool", arguments={})],
),
LLMResponse(text="Done"),
]
)

class _DummyApp:
state = SimpleNamespace(agent_override=None)

request = SimpleNamespace(app=_DummyApp())

agent = get_agent(
request=request, # type: ignore[arg-type]
settings=settings,
llm=fake_llm,
registry=registry,
memory=InMemoryMemoryStore(),
trace=InMemoryTraceSink(),
)

await agent.run("Run long tool")

# fake_llm.history records list of messages passed on each turn
all_messages = [msg for turn in fake_llm.history for msg in turn]
tool_msg = next(m for m in all_messages if m.role.value == "tool")
assert tool_msg.content.startswith("A" * 40)
assert "[Output truncated: exceeded 40 character limit]" in tool_msg.content


def test_docker_compose_environment_covers_all_settings() -> None:
"""AUDIT-12: docker-compose api environment declares all settings in Settings."""
compose_path = Path(__file__).parents[2] / "docker-compose.yml"
with open(compose_path, encoding="utf-8") as f:
compose_data = yaml.safe_load(f)

api_env = compose_data["services"]["api"]["environment"]
env_keys: set[str] = set()
for item in api_env:
key = item.split("=")[0].strip()
env_keys.add(key)

settings_fields = set(Settings.model_fields.keys())
missing = settings_fields - env_keys
assert not missing, f"Missing settings in docker-compose.yml: {missing}"


def test_spec_documents_all_settings() -> None:
"""AUDIT-12: docs/SPEC.md and docs/AGENTKIT_SPEC.md document all settings."""
spec_dir = Path(__file__).parents[2] / "docs"
for filename in ["SPEC.md", "AGENTKIT_SPEC.md"]:
path = spec_dir / filename
content = path.read_text(encoding="utf-8")
for field in Settings.model_fields:
assert f"`{field}`" in content, f"Setting {field} missing from {filename}"