Repository navigation
fix(audit): remediate whole-project audit findings - #47
Merged
Merged
Conversation
Addresses 17 findings from a full-codebase audit. Several paths converted a
misconfiguration or a transient fault into a working-looking success.
Fail-closed configuration and dependency resolution:
- Settings.API_KEYS no longer defaults to a key published in the repository;
validation rejects an empty or whitespace-only value.
- get_llm_client no longer falls back to FakeLLMClient. Provider construction
failure raises ServiceUnavailableError (503). LLM_PROVIDER=fake remains an
explicit opt-in that short-circuits before the factory.
- get_agent builds the Agent per request instead of memoizing the first
assembly on app.state, which pinned every later request to the first
request's LLM client, registry, memory store and trace sink.
- Wired RetryingLLMClient into the request path (previously dead code) with
new LLM_MAX_RETRIES / LLM_RETRY_BASE_DELAY_S / LLM_RETRY_MAX_DELAY_S.
- TieredMemoryStore forwards get_run_trace and list_runs to the run store, so
the trace endpoint no longer falls through to an empty trace sink.
- examples/demo.py requires --offline for scripted execution rather than
silently downgrading on a missing key.
Post-connect SSRF verification and bounded resource use:
- http_fetch resolves DNS via asyncio.to_thread and reuses those addresses.
- Verifies the connected socket's server_addr against the validated addresses,
closing the DNS-rebinding window between validation and connection.
- Streams the body under an explicit byte budget instead of buffering then
slicing.
- sql_readonly applies the row limit in SQL, pushes statement_timeout to
PostgreSQL, masks string literals and quoted identifiers before its keyword
scan, and allows the #> / #>> / #- JSON path operators.
Atomic fallbacks and domain-error boundaries:
- The rate limiter's non-scripting fallback uses WATCH/MULTI/EXEC optimistic
locking instead of a trim/count-then-insert split. Against live Redis the old
version admitted 39 of 50 simultaneous requests against a limit of 5; the new
one admits exactly 5.
- Both LLM adapters wrap response parsing in the error boundary, so blocked
prompts, empty choices and malformed tool-argument JSON raise
LLMProviderError rather than escaping as raw SDK exceptions.
- Generated tool_call_ids use full UUIDs instead of 32-bit truncations.
- RetryingLLMClient.__getattr__ no longer recurses on a partial instance.
- /health returns fixed client-facing strings and logs the driver error, so the
unauthenticated endpoint stops disclosing hostnames, ports and database names.
Smaller fixes: fixed the RedisMemoryStore.truncate_messages off-by-one that
returned max_messages + 1 entries with a system prompt; stopped logging raw run
goals at INFO; replaced split(";") in scripts/seed.py with a statement-aware
splitter handling literals, comments and dollar-quotes; bounded calculator
exponentiation by result bit length.
Suite: 277 hermetic passed, 25 real-infra passed against live PostgreSQL and
Redis. Coverage 92% overall. ruff check, ruff format --check and
mypy agentkit tests are clean.
… timeout - verify_peer_address tolerates malformed server_addr without crashing and fails closed on restricted or unparseable peer hosts. - Run the comment and multi-statement scans on the literal-masked query so markers inside string data no longer cause false rejections. - Prefer appending LIMIT over subquery wrapping; wrap only for row-locking tail clauses. Add FETCH FIRST to the existing-bound detection. - MySQL timeout uses SET SESSION MAX_EXECUTION_TIME, which actually covers subsequent statements, instead of a no-op SET STATEMENT ... FOR SELECT 1.
…tion Adapt the rebased audit tests to the AUD-01/AUD-04 behaviour now on main: deps delegate to the factory instead of wrapping retry itself, build_llm exits via SystemExit on a missing key, and the per-request probe test uses in-memory memory/trace overrides so it stays hermetic without Redis.
getdownandcode
force-pushed
the
fix/audit-hardening
branch
from
October 18, 2026 17:07
32538a3 to
e6ead1f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Remediates 17 findings from a whole-project audit. The theme: several paths converted a misconfiguration or a transient fault into a working-looking success — HTTP 200 with fabricated data where there should have been a 503.
Rebased onto the AUD-01/AUD-04/AUD-05 follow-ups that landed while this branch was open; overlapping fixes were reconciled with main and the decisions recorded in ADR-009/010/011.
Closes #47 tracking
M13-T06.Critical
Settings.API_KEYSno longer defaults to a key published in the repository (ak_test_key_12345). Validation rejects an empty or whitespace-only value, so a deployment without a.envfails at startup instead of serving unauthenticated traffic. The compose migrations service now receivesAPI_KEYStoo, since alembic andscripts/seed.pyconstructSettings.get_llm_clientno longer falls back toFakeLLMClient. Provider construction failure raisesServiceUnavailableError(503). Previously a missingGEMINI_API_KEYproduced HTTP 200 with scripted text.LLM_PROVIDER=fakeremains an explicit opt-in resolved by the factory.app.state.agent_override, so the contract is distinguishable from the memoized first assembly removed in AUD-01.RetryingLLMClientintocreate_llm_client_from_settings; this PR replaces the singleMAX_RETRIESwithLLM_MAX_RETRIES,LLM_RETRY_BASE_DELAY_SandLLM_RETRY_MAX_DELAY_Sand passes all three through the factory. Deps stay thin and return the factory client unchanged, so the retry budget is never doubled.TieredMemoryStoreforwardsget_run_trace/list_runs. The trace endpoint duck-types onget_run_trace, so with aTieredstore it fell through to a trace sink that may hold no rows and returned an empty trace despite rows existing.http_fetch: post-connect SSRF verification. Validation resolved the host and checked the addresses, then lethttpxresolve again — a short-TTL DNS answer could hand back a public address during validation and a loopback or169.254.169.254address during connection. The connected socket'sserver_addris now checked against the validated addresses.http_fetch: no blocking DNS, bounded body.socket.getaddrinforan inline on the event loop; it moves toasyncio.to_thread.response.textbuffered the whole body before slicing tomax_chars; the body is now streamed under an explicit byte budget.WATCH/MULTI/EXECversion admits exactly 5. The AUD-10 in-memory fallback for Redis outages is untouched and still applies toRedisErrors.Moderate
raw_response.textraises on a blocked prompt and an emptychoices/candidateslist raisedIndexError, both escaping as raw SDK exceptions and breaking the error taxonomy.{"raw": ...}, which then failed argument validation with a misleading "missing required field" message.tool_call_ids.str(uuid4())[:8]is 32 bits and collision-prone within a long session; the id is the join key between an assistant tool call and its result.RetryingLLMClient.__getattr__no longer recurses when_clientwas never assigned./healthreturns fixed client-facing strings and logs the driver error.str(exc)from SQLAlchemy and Redis embeds host, port and database name, on an unauthenticated endpoint.sql_readonly: row limit applied in SQL.fetchmany(row_limit)alone still makes the database plan and execute the full scan.statement_timeoutis now pushed to PostgreSQL. The keyword scan masks string literals and quoted identifiers first, soWHERE action = 'insert'is no longer rejected, and#>/#>>/#-are recognised as JSON path operators rather than comment markers.Minor
RedisMemoryStore.truncate_messagesoff-by-one.[system_msg, *non_system[-max_messages:]]returnedmax_messages + 1entries whenever a system prompt was present, so the bound did not hold.max_messagesis now a total budget, as the parameter name implies.core/agent.pyandapi/routes.py.scripts/seed.pyuses a statement-aware splitter handling string literals, line/block comments and dollar-quoted bodies, instead ofsplit(";").(999**100)**5; every individual exponent stays withinMAX_EXPONENTwhile the result reaches ~5000 bits.examples/demo.pyrequires--offlinefor scripted execution rather than silently downgrading on a missing key, and raises aSystemExitwith a CLI-appropriate message. Offline runs also skipget_settings()entirely, so the demo works with no services and no.env.Verification
Hermetic tier, post-rebase:
The real-infrastructure tier (
AGENTKIT_REAL_INFRA=1) runs in CI's Test Against Live PostgreSQL and Redis job, including the newtests/real/test_redis_real.pyregression test that asserts exactly 5 admissions from 50 concurrent calls to the non-scripting fallback. It was verified to fail against the old implementation.New
tests/unit/test_audit_regressions.pypins each fix, including the deps delegation contract (no double retry wrap), the CLI-strict demo behaviour, and the health-endpoint leak check.Notes for review
API_KEYSis now required everywhere.tests/conftest.pyseeds a fixed value beforeagentkit.api.mainis imported, since that module builds an app at import time..env.example,README.mdanddocs/DEPLOY.mdupdated; CI already setsAPI_KEYSin both jobs.MAX_RETRIESis renamed toLLM_MAX_RETRIES; the two delay settings are new.app.state.agentwas renamed toapp.state.agent_overrideto make the override intent explicit now that it is not a cache.http_fetchtests driveAsyncClient.streamrather thanAsyncClient.get, following the tool's move to streaming. Two existing tests asserted the buggymax_messages + 1truncation and were corrected.