Resolution order for the URL embedded in the install one-liner is now:
1. LITELLM_CLOUD_AGENT_PROXY_BASE_URL env var (operator-configured,
fully trusted) — recommended for production.
2. X-Forwarded-Host / X-Forwarded-Proto, ONLY when the operator
opts in via LITELLM_TRUST_PROXY_HEADERS=1.
3. The request's direct Host header — safe by default because it
reflects the actual TCP destination, not an attacker-supplied hop.
Previously any authenticated caller could forge X-Forwarded-Host to
embed an attacker-controlled URL in the install command. If a second
operator ran that command, the worker would send its raw pair token to
the attacker's host, who could then call POST /v2/agent-workers/register
and gain a long-lived worker JWT.
Also adds structured logging on /register failures (invalid / replayed
/ expired tokens) so operators running the proxy behind a WAF / fail2ban
can detect abuse at the network layer (the proxy itself doesn't ship a
built-in per-IP limiter).
Locks the production-safe default for LITELLM_CLOUD_AGENT_MOCK_AWS so a
future revert to the unsafe "1" default trips CI. Also covers the
strict-string parsing — typos like "true" / "yes" must NOT silently
flip on the mock.
Previously the test-connection endpoint defaulted to mock-on, so a fresh
production proxy would silently return a synthetic success for any
non-empty AWS access key. Operators saving incorrect credentials would
only discover the failure later when VMs failed to launch.
Default is now "0" — operators must set LITELLM_CLOUD_AGENT_MOCK_AWS=1
explicitly to opt into the mock path during local development.
Also adds an inline comment on _build_update_payload's `is not None`
guard so a future contributor doesn't silently drop `False` / `0`
updates by switching to truthy comparison.
Adds three regression cases that fail if a future change drops the
shlex.quote() pass on proxy_url, raw_token, or install_script_url. The
existing simple-input cases still pass unchanged because shlex.quote
returns alnum/colon/slash/dot strings verbatim.
shlex.quote() the install_script_url, proxy_url, and raw_token before
interpolating into the curl-pipe-sh one-liner. Without quoting, a
proxy_url containing spaces or shell metacharacters (e.g. via a misconfig
or the X-Forwarded-Host issue Greptile also flagged) could produce a
malformed or exploitable command on the worker box.
Greptile P2: a misbehaving or adversarial server returning
'Retry-After: 9999999' could stall the SDK indefinitely. Cap the
honored delay at MAX_RETRY_AFTER_MS (60s).
Greptile P1: the reconnects counter accumulated across the entire
stream lifetime — for a long-running stream with several transient
drops over hours, the budget would be exhausted even though every
individual reconnect succeeded. Now the counter tracks *consecutive*
failures: once a connection delivers at least one new event, the
counter resets to zero on the next drop, so only a sustained outage
trips sse_reconnect_exhausted.
Greptile P1: wait() polled indefinitely with no escape hatch — a stuck
or partitioned server would hang the caller forever. Now accepts
{ signal, timeoutMs, pollMs } and throws LiteLLMAgentError with codes
wait_aborted / wait_timeout. Forwards the signal to the underlying
requestJson call and to the inter-poll sleep.
Greptile P2: runFromInfo was exported but had no internal callers
(agent.ts and session.ts both build Run directly). Removed to shrink
the surface and avoid leaking resolveClient as a construction detail.
@ant-design/icons isn't a direct dependency of the dashboard. Use a
text glyph (▸/▾) instead — keeps the toggle visible without adding
a runtime import.
Optional-chaining dayjs(...).fromNow?.() was a TS error because the
relativeTime plugin wasn't loaded. Use relativeOrAbsolute() from the
shared helper instead.
Centralizes dayjs.extend(relativeTime) so fromNow() is typed and
loaded across the agents components. relativeOrAbsolute() falls back
to '—' for null/invalid timestamps so callers don't have to
re-implement the guard.
Exercises the auth gate plumbing — present a fake token, navigate, then
swap to a fresh token and reload. Real backend partitioning is gated
on Epic A; the spec is structured so its assertions can be tightened
once the real /v2/ endpoints land.
Type a message, send; assert the user_message bubble count strictly
grows. Mock provider acks the user_message synchronously — that's
enough to verify the composer plumbing without Epic A.
Mid-stream, page.context().setOffline(true)/(false); event count never
regresses and continues climbing after reconnect. Exercises the seq
dedup branch of useSessionEventStream.
Inside a session, poll until ≥3 events have rendered in the conversation
pane (combination of message bubbles and tool-call cards). Mock provider
ticks every 400ms so this lands well under the 10s budget.
Routes load — /agents, /agents/{aid}, /agents/{aid}/sessions/{sid}
each render their primary container. Captures console errors and
asserts none on the list view.
useAuthorized requires an unexpired JWT in the `token` cookie before it
renders. We mint an unsigned 1-hour token here — jwt-decode never
verifies the signature, so any structurally valid base64 payload works.
Helper plus an AGENTS_DEV_URL constant (overridable via env).
Targets http://localhost:3000 (Next.js dev server) directly. The agents
UI lives in App Router routes which only render under `next dev`, not
the proxy's static export — separate config skips the proxy globalSetup
that the rest of the suite needs.