Skip to content

feat: bundle agentgateway in Docker image + plit init/api integration#26

Open
theuseless-ai wants to merge 18 commits into
mainfrom
feat/agentgateway-docker
Open

feat: bundle agentgateway in Docker image + plit init/api integration#26
theuseless-ai wants to merge 18 commits into
mainfrom
feat/agentgateway-docker

Conversation

@theuseless-ai

Copy link
Copy Markdown
Contributor

Closes #25 (Phase 1: bundle agentgateway in the Docker image, optional).

Summary

Integrates agentgateway (the MCP/LLM sidecar for the PLIT stack) into plit as an optional, self-hosted component:

  • Dockerfile: gated download of the agentgateway binary behind ARG INCLUDE_AGENTGATEWAY (same pattern as Dragonfly).
  • docker/entrypoint.sh: starts agentgateway alongside the rest of the stack when bundled.
  • plit init (src/commands/init/agentgateway.rs + scripts): generates agentgateway config, assembles it (assemble-config.sh), and resolves credentials (decrypt_keys.py). Config is emitted as JSON and passed with -f; listeners use HTTP with bind/port and env-style backendAuth.
  • src/commands/api.rs: new api command surface (groundwork that also feeds plit api subcommands — CLI wrapper over generated Pipelit client #18).
  • .gitignore: ignore .env and .omc.

13 commits, +1072/-2 across 12 files. Branch is cleanly ahead of main (fast-forwardable).

Provenance / review notes

  • The final commit (b4cd039) is a rescue of uncommitted WIP recovered from a decommissioned machine. It has been compile-verified (see below) but the integration history is iterative ("fix: …" churn while dialing in the agentgateway config format) — reviewers should sanity-check the final config shape rather than each intermediate commit.
  • ⚠️ Security review point: decrypt_keys.py now falls back to plaintext when FIELD_ENCRYPTION_KEY is invalid. Please confirm this fallback is acceptable (it silently bypasses field encryption) or gate it behind an explicit dev-only flag.

Test plan

  • cargo check passes locally (workspace, 34s).
  • CI: workspace build + tests.
  • CI / manual: docker build --build-arg INCLUDE_AGENTGATEWAY=true produces an image with the agentgateway binary, and entrypoint.sh starts it.
  • Manual: plit init generates a valid agentgateway config that the binary accepts (agentgateway -f config.json).

🤖 Generated with Claude Code

aka and others added 18 commits May 25, 2026 15:41
- plit init bootstraps agentgateway config structure + ES256 keypair
- Generates JWKS public key for JWT validation
- Writes config fragments (listeners, rules, backends) from LLM choice
- Embeds assemble-config.sh, start.sh, decrypt_keys.py scripts
- Appends AGENTGATEWAY_ENABLED/URL/DIR + JWT_PRIVATE_KEY to .env
- Dockerfile includes agentgateway binary (v1.0.1) + yq
- Entrypoint starts agentgateway before Pipelit stack
- Exposes ports 4000 (LLM), 3000 (MCP), 15000 (admin)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway v1.0.1 uses HTTP protocol for all listeners (not LLM/MCP).
Also fix hostOverride to contain just the hostname, not the full URL.
Extract path from base_url for openai-compatible providers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway v1.0.1 expects 'key' not 'apiKey' for backend auth config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway expects key: { env: "VAR_NAME" } not key: { envKey: "VAR_NAME" }.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Work around serde_yaml bug where untagged enum FileOrInline fails
to deserialize key.env from YAML but works from JSON.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway expects address on the bind, not on the listener.
The assemble-config.sh adds each listener file as a bind entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The -c flag may have argument length limits. Use -f with the
converted JSON file instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway expects 'port' (integer) at the bind level, not 'address'.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agentgateway's -f flag uses serde_yaml which doesn't support the
env variant of FileOrInline. Resolve env references to plain strings
using Python before starting agentgateway.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Recovered from the old Alienware disk before decommissioning.
Touches the Docker entrypoint, the init agentgateway command, and
decrypt_keys.py (adds plaintext fallback when FIELD_ENCRYPTION_KEY
is invalid).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the soft "probe ~10s then continue" with a blocking readiness gate that exits non-zero (FATAL) if agentgateway never becomes healthy, so plit never serves without the gateway. Exports AGENTGATEWAY_DIR/URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t in config gen

- assemble-config.sh: skip the provider.<x>.model override when the model file declares none (pass-through), and include backendTLS only when the provider fragment declares it.

- agentgateway.rs write_provider_yaml: emit backendTLS only for https upstreams, so plain-HTTP backends (LAN Qwen, local Ollama) are reachable instead of failing with a TLS error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
provider_name_from_url picked the 2nd-level 'domain' via split('.').rev().nth(1), which for an IP host like 192.168.0.73 yields the octet '0' -> provider name '0' -> invalid env var '0_API_KEY', so start.sh's export fails and agentgateway never starts (fatal under the hard boot dependency). Now sanitize to a valid identifier: real hostnames still map api.openai.com->openai, but IPs/local hosts become p_192_168_0_73 / localhost. Found via a live Colima e2e against a LAN openai-compatible (Qwen) backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…way route

plit init created the agentgateway route (<provider>-<model_slug>) but never told pipelit; the default node's backend_route was NULL, so pipelit fell back to a mismatched name and its proxied LLM call 404'd ("route not found") at the gateway. plit init now computes the route (new agentgateway::initial_route_name) and passes it via `apply-fixture --backend-route`. Verified end-to-end: real chat -> agentgateway -> LAN Qwen box returns 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No rate limiting or spend guard existed anywhere, leaving autonomous
agent loops able to cause runaway LLM spend. agentgateway v1.0.1 has
no dollar-budget feature, so use route-level localRateLimit token
buckets (schema verified against the v1.0.1 tag).

- write_rate_limits() seeds config.d/policies/rate-limit.yaml only if
  absent (operator tuning survives re-init); generous defaults:
  300 req/min (600 burst) + 200k tokens/min (400k burst) per route
- assemble-config.sh injects it as policies.localRateLimit on every
  LLM route; absent/empty fragment -> byte-identical output to before
- emit the fragment as JSON with a `// []` fallback so an empty,
  null, or comment-only file collapses to [] (a comment-only file
  would otherwise inject localRateLimit: null, which agentgateway
  rejects at startup and bricks the hard-boot dependency)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 1: Bundle agentgateway in Docker image (optional)

2 participants