Commit graph

21 commits

Author SHA1 Message Date
Sameer Kankute
157e7a0f20
fix(containers): record ownership for service-account keys + fix Prisma Json serialization (#28990)
* fix(containers): record ownership for service-account keys + fix Prisma Json field serialization

- Track containers created implicitly via /v1/responses by extracting container IDs
  from the response output and calling record_container_owner for each one, so
  subsequent file-API calls from the same service account pass ownership checks.
- Fix DataError: Prisma Python requires Json fields to be JSON strings; serialize
  file_object with json.dumps() before insert/update in LiteLLM_ManagedObjectTable.
- Add collect_container_ids_from_responses_response utility to responses/utils.py
  that walks all output item shapes (code_interpreter_call, message annotations).
- Tests: two new cases covering the responses-tracking path and the end-to-end
  record-then-assert flow for service accounts with team scope.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(containers): swallow all exceptions in ownership hook; tighten file_object_json type to str

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(containers): parse file_object JSON string in existing ownership test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: container ownership recording bugs

- Remove unreachable _aresponses_websocket from route_type set in
  base_process_llm_request; the WebSocket endpoint never flows through
  base_process_llm_request, so this branch was dead code that gave a
  false impression of coverage.
- Drop the HTTPException re-raise in record_container_owners_from_responses_response
  so per-container failures (including HTTP 403/500 from conflicting
  ownership rows) no longer abort the batch and skip recording for the
  remaining container IDs in the same response.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(containers): record ownership for streaming /v1/responses too

Streaming /v1/responses returns through the select_data_generator
branch in base_process_llm_request and bypasses the non-streaming
ownership tail, so code-interpreter containers created mid-stream
were never written to LiteLLM_ManagedObjectTable. Follow-up file API
calls would then 403.

Wrap the SSE generator so container ownership is recorded once the
upstream iterator finishes assembling completed_response. Also covers
the background-polling path, which loops body_iterator end-to-end.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-27 21:00:07 -07:00
user
4699b3dc81
chore(container): use delete_cache, json-encode scope key, clean test
/simplify follow-ups:

* Replace the two-``pop`` reach into ``cache_dict``/``ttl_dict`` with
  the existing public ``InMemoryCache.delete_cache(key)`` — the same
  idiom used elsewhere in the proxy. Bonus: ``delete_cache`` calls
  ``_remove_key`` which also handles ``expiration_heap`` consistency
  the direct pops were silently leaking.

* JSON-encode the sorted scope list for the cache key instead of
  ``"|".join``. ``user_id`` / ``team_id`` / ``org_id`` / ``api_key``
  are free-form strings and could contain a literal ``|`` — JSON
  quoting escapes any in-string separator unambiguously.

* Extract ``_allowed_container_ids_cache_key()`` so the read and
  invalidation sites compute the key the same way.

* Fix a placeholder-then-overwrite test construction: the
  ``__module__.split(".")[0] and "proxy_admin"`` line evaluated to a
  literal string that was immediately overwritten with the real enum
  value. Hoist the import and construct directly.
2026-05-05 00:43:47 +00:00
user
2adfa96db2
fix(container): cache list-allow-set, track admin-created containers
Address Greptile P2 follow-ups from the prior round:

* Cache ``_get_allowed_container_ids`` (60s LRU/TTL keyed by sorted
  owner-scope tuple) so ``GET /v1/containers`` doesn't issue a fresh
  ``find_many`` against ``litellm_managedobjecttable`` on every list
  call. Invalidate the caller's own cache entry when they record a
  new owner so the just-created container shows up on their next list.

* Tighten the admin early-return in ``record_container_owner`` to skip
  ONLY when there's literally no container ID to stamp. An admin with
  identity (the master-key path populates ``user_id`` + ``api_key``)
  flows through the normal record path so admin-created containers are
  tracked like any other caller's. The truly-identity-less admin case
  still falls through to the 403 below — correct fail-secure default.

Skill-cache invalidation gap (also flagged by Greptile) is moot: there
is no skill update endpoint exposed; ownership-affecting mutations are
only delete (already invalidates) and create (new ID, no cache entry
to update).
2026-05-05 00:39:53 +00:00
user
6ce84effe1
chore: simplify ownership tracking — drop thin stores, in-memory fallback, hand-rolled cache
Substantial reduction (~765 LOC) without changing the security
boundary:

* Drop ContainerOwnershipStore and LiteLLMSkillsStore — both were
  one-method-per-Prisma-call wrappers. Inline the calls instead,
  matching the established pattern in vector_store_endpoints,
  agent_endpoints, and mcp_server/db.py.

* Drop the prisma_client is None in-memory fallback. Production
  deploys always have Prisma; running ownership-critical paths on a
  process-local dict is a security footgun in the dev-mode case it
  was meant to support, and complicates every code path with a
  branch. Fail-secure: skip recording if Prisma is unavailable, and
  treat reads as "not found" (admin-only).

* Drop the hand-rolled module-level cache. Replace with the existing
  litellm.caching.in_memory_cache.InMemoryCache, which already has
  TTL + max-size + eviction tested in its own module. Sentinel string
  for negative caching since InMemoryCache can't disambiguate "miss"
  from "cached as None".

* Tests: drop coverage for removed code paths (in-memory fallback,
  hand-rolled cache internals). Keep tests for actual behavior (cache
  hit-rate, negative caching, owner check, list filtering,
  identity-less reject, admin bypass).
2026-05-05 00:23:32 +00:00
user
758b488326
fix(ownership): reject identity-less callers instead of sharing a sentinel scope
UNSCOPED_RESOURCE_OWNER_SCOPE collapsed every caller without an
identity field (no user_id / team_id / org_id / api_key / token) into
a single shared owner — a cross-tenant access primitive: any two such
callers could see and delete each other's containers and skills.

Drop the sentinel. ``get_primary_resource_owner_scope`` returns
``None`` and ``get_resource_owner_scopes`` returns ``[]`` for
identity-less callers. ``record_container_owner`` and
``LiteLLMSkillsHandler.create_skill`` now reject creates from
identity-less callers with a 403 instead of stamping the placeholder.
Read paths already deny ``owner is None`` correctly so legacy rows
(if any) are admin-only.
2026-05-04 23:40:22 +00:00
user
de682c810e
chore(container,skills): drop legacy-access opt-out env vars
LITELLM_ALLOW_UNTRACKED_CONTAINER_ACCESS and
LITELLM_ALLOW_UNOWNED_SKILL_ACCESS were operator-toggleable opt-outs
for the cross-tenant access primitive this PR closes — flipping either
on re-enabled exactly the VERIA-20 read path. Default-secure with no
escape hatch matches sibling fixes (vector-store cred isolation, semantic
cache key isolation, user_config strip): all rejected the
opt-out-of-security pattern.

Untracked containers and unowned skills (rows that pre-date this
enforcement) are admin-only. Non-admin owners need to either re-create
via the now-tracked flow or have an admin assign ``created_by`` on the
existing row. Update tests to assert the strict-only behaviour.
2026-05-04 23:22:19 +00:00
user
ec9b84d38c
chore(container,skills): LRU eviction for owner caches; widen file_purpose Literal
Two cleanups from the /simplify pass:

* ``_CONTAINER_OWNER_CACHE`` and ``_SKILL_CACHE`` now LRU-evict via
  ``OrderedDict.popitem(last=False)`` instead of full ``clear()`` at
  capacity. Full clears converted a steady-state cached workload into a
  periodic full-DB-load oscillation as the cache repopulated from zero
  and cleared again. Reads now ``move_to_end`` so the just-touched
  entry survives the next eviction. Mirrors the pre-existing LRU
  pattern in ``_remember_container_owner``.

* ``LiteLLM_ManagedObjectTable.file_purpose`` Literal now includes
  ``"container"`` so Pydantic validation accepts rows written by the
  ownership store.
2026-05-04 22:52:54 +00:00
user
c01f209723 fix(proxy): forward decoded container ids after ownership checks 2026-05-01 21:50:57 -07:00
user
6194028f79
perf(proxy): cache container/skill ownership reads on the hot path
Container ownership and skill rows are looked up on every retrieve /
delete / list / file-content / chat-completion-with-skill call. The new
stores wrapped raw Prisma queries with no cache, putting one DB
round-trip on each request. Add an in-process TTL'd cache mirroring the
_byok_cred_cache pattern in mcp_server/server.py: per-key (value,
monotonic_timestamp), 60s TTL, 10000-entry cap with full-clear on
overflow, invalidated by every write. Negative results (`None`) are
cached too so untracked-resource checks also skip the DB.

Tests cover: cache-after-first-hit, negative caching, write
invalidation, no-caching-on-DB-error, TTL expiry, capacity eviction.
56 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 03:26:58 +00:00
user
6c37e9d8d5
fix(proxy): handle ownership-recording failures after upstream create
If record_container_owner raises after the upstream container is created,
the user previously got a 500 with no usable container — they were billed
for an unreachable resource. Move ownership recording into the create
path's exception handling and split the two failure modes:

- HTTPException from the recorder (auth conflicts) propagates verbatim
  so the client sees the real status code, not a generic LLM error.
- Unexpected exceptions are logged and swallowed; the response is
  returned to the caller so they aren't billed for a container they
  can't address. The DB row stays untracked until an operator reconciles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 02:23:45 +00:00
user
d812a5356e fix managed container routing after staging merge 2026-05-01 11:44:33 -07:00
user
f18ee0319d fix(proxy): isolate ownership persistence paths 2026-04-30 20:25:40 -07:00
user
3a566c3938 fix(proxy): stabilize ownership fallback and openapi ids 2026-04-30 19:55:00 -07:00
user
20eb9c96ca fix(proxy): avoid mutating container responses 2026-04-30 19:43:20 -07:00
user
7b1e3f278b test(proxy): cover container endpoint post processing 2026-04-30 19:34:20 -07:00
user
f3cff9338e fix(proxy): harden resource ownership fallbacks 2026-04-30 19:29:08 -07:00
user
6aac4552f9 fix(proxy): forward decoded container ids 2026-04-30 19:13:20 -07:00
user
9376b30bca fix(proxy): reset filtered container pagination 2026-04-30 19:01:37 -07:00
user
0745b1872e test(proxy): cover container endpoint forwarding 2026-04-30 18:46:14 -07:00
user
d2a322074f fix(proxy): preserve container ownership compatibility 2026-04-30 18:40:26 -07:00
user
ad9aa43e86 chore(proxy): scope skills and container resources 2026-04-30 18:23:58 -07:00