feat(agentgateway): trust-boundary hardening — remove admin escalation, tighten health check, add rate limits#193
feat(agentgateway): trust-boundary hardening — remove admin escalation, tighten health check, add rate limits#193theuseless-ai wants to merge 10 commits into
Conversation
- JWT issuer (ES256) for server-to-server auth with agentgateway - Proxy client routing LLM calls through agentgateway - Config writer for provider/model subfolder structure - Providers API (admin CRUD) and available-models API (all users) - LLM resolution via backend_route with llm_credential_id fallback - Migration script (cli migrate-credentials) - Frontend: Providers page, model picker dropdown, credentials page cleanup - Auto-restart agentgateway on provider add/remove - Feature-flagged via AGENTGATEWAY_ENABLED Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-upstream TLS - add_model no longer writes a display name into provider.<x>.model (a hard outbound override) -> leave unset for caller model pass-through; strip "null" sentinels post-assembly. - migrate/config: multiple keys for one provider now fail loud instead of silently keeping the first. - _parse_base_url carries the URL scheme -> add_provider(use_tls); backendTLS emitted only for https so plain-HTTP upstreams (LAN Qwen, Ollama) are reachable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se 1b) Removes every raw-key code path from pipelit; workflow code holds only a 60s ES256 JWT that agentgateway swaps for the real key. - llm.py: delete direct-provider fallback (agentgateway is the only path; defensive raise if disabled); replace _fake_credential_from_route (api_key="") with RouteProviderInfo (provider_type + base_url, no key). - _agent_shared.py / dsl_compiler.py: stop reading credential.api_key; dsl sources models from agentgateway. - api/credentials.py: LLM credential CRUD/test/models -> 410 (git/gateway/tool intact). - alembic a41f9c2d7e10 + models/credential.py: drop llm_credentials.api_key column. - config.py: AGENTGATEWAY_ENABLED defaults True; main.py: fail-fast startup health guard; conftest: disabled for tests. - cli migrate-credentials: deprecation stub (keystore emptied). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntgateway - CredentialsPage: drop the LLM credential form/type entirely (git/gateway/tool remain). - NodeDetailsPanel: model picker + native-search detection now source from agentgateway available-models, not credential rows. - credentials.ts: drop the removed LLM test/models hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st boundary) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ateway route Accept an optional --backend-route and store it on the default-agent ai_model node config, so pipelit's create_proxied_llm calls the agentgateway route plit init created instead of a mismatched fallback name (which 404'd). Additive and backward-compatible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Asserts the flag sets the ai_model node's backend_route (so pipelit's agentgateway call matches plit init's route), and defaults to None when omitted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolve_llm_for_node never set a JWT role, so every workflow LLM call hit the `role = user_role or "admin"` fallback and minted an ADMIN token — bypassing the gateway's CEL rules. Root cause was a role-vocab mismatch (DB stores admin/normal; gateway rules accept admin/user), so admin was the only role that passed for workflow traffic. - add _jwt_role_for_gateway(): admin -> admin; everything else (normal, None, empty, unknown) -> least-privilege user - resolve_llm_for_node now looks up the workflow owner's role and plumbs it through; lookup failure degrades to user, never admin - invariant: no code path where a missing role yields an admin token - tests: least-privilege default, explicit-admin, unknown-role, owner-role threading, str-enum handling Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
check_agentgateway_health returned True for ANY HTTP status, so a 500 from agentgateway passed the hard-boot dependency guard in main.py. - healthy = 2xx or 401/403 only (401/403 preserved as 'gateway up, rejecting unauthenticated probe'); 5xx and unexpected statuses -> unhealthy. 3xx now unhealthy too (httpx doesn't follow redirects). - tests: 500, 503, 404 unhealthy cases Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
FastAPI 0.139 / Starlette 1.3 include routers lazily — app.routes holds unmaterialised _IncludedRouter stubs (no .path) until the route table is finalised on first request, so the old assertion saw zero /api/ routes and failed. Pre-existing latent failure, surfaced by this branch's first CI run; unrelated to the JWT/health/rate-limit changes. Assert against app.openapi()['paths'], which forces materialisation and reflects the real route table (72/74 paths under /api/). App itself was never broken — e2e-smoke exercises these routes live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|

Phase 1b hardening from an architecture review of the agentgateway trust boundary. Three P0 fixes; all changes validated, and the security change was adversarially reviewed against the real agentgateway v1.0.1 binary.
What & why
1. Remove default-admin JWT escalation (security)
resolve_llm_for_nodenever set a JWT role, so every workflow LLM call hit therole = user_role or "admin"fallback and minted an admin token — bypassing the gateway's CEL rules. Root cause: a role-vocabulary mismatch (DB storesadmin/normal; the deployed CEL rules acceptadmin/user), soadminwas the only role that passed for workflow traffic._jwt_role_for_gateway():admin→admin; everything else (normal,None, empty, unknown) → least-privilegeuserresolve_llm_for_nodenow looks up the workflow owner's role and plumbs it through; lookup failure degrades touser, never admin2. Tighten agentgateway boot health check
check_agentgateway_healthreturnedTruefor any HTTP status, so a 500 passed the hard-boot dependency guard. Now healthy = 2xx or 401/403 only; 5xx / unexpected / 3xx → unhealthy.3. Starter local rate limits (companion PR: plit #26)
No spend guard existed anywhere. Adds route-level
localRateLimittoken buckets (schema verified against the v1.0.1 tag; v1.0.1 has no dollar-budget feature). Generous defaults, non-disruptive: absent fragment → byte-identical config. The plit-side change is in theuselessai/plit#26.Validation
Not in scope (follow-ups)
normalvs gatewayuser) — handled by a mapping fn; single change point if unified laterconfig.d/writers🤖 Generated with Claude Code