Commit graph

1016 commits

Author SHA1 Message Date
Danny Avila
f435cb2a80 💅 style: Reformat a test helper signature
Formatting only. The line was edited after the last gate run and committed
without re-running it, which failed ESLint and Prettier in CI.
2026-08-31 21:39:16 -04:00
Danny Avila
9044dedcb2 🧰 fix: Move upload destination policy into shared code and share one size accounting
Destination and acceptability were decided by a block of new policy in the legacy
file service: promote a text-routed upload to context, find a tool that could
consume one kept off the model path, refuse one that nothing could read or that
would land on no agent resource. That is backend policy rather than wiring, and
the workspace rules put it elsewhere.

It now lives in data-provider as one pure function, next to the delivery-path
resolution it follows. That is also the only home reachable from here: the file
service cannot import from packages/api, because a suite mocks that package with
an explicit allowlist and any real import becomes undefined under test. The rules
are provider-agnostic policy either way, so this is where they belong rather than
merely where they fit.

The earlier deduplication of the shared size allowance covered the deferred
filter and not the persistent screening, which recomputed the budget from both
raw arrays and could silently drop a persistent file that fits. Both now use one
accounting that counts a file appearing in several sets once.
2026-08-31 21:31:24 -04:00
Danny Avila
76d4bd6929 🔐 fix: Scope provisioning uploads to the agent's own files, and charge shared files once
A file's context said where its vectors already live, and that was also being used
to decide where new provisioning may write. Those are different questions. A user
who owns a context file from one agent can attach it to a conversation with
another, and request hydration checks only ownership, so the upload was performed
under the second agent's shared code and vector identity. Its other users could
then reach the first agent's setup file.

Upload identity now comes from membership in the active agent's own resources,
carried on the provisioning state. Reading where content already lives still uses
the record's context, which is what it actually records; conflating the two broke
four tests that were right to break, since a code output listed among an agent's
context files has its vectors under the user.

Separately, a file can appear both in the delivery set and among the provisioning
candidates, an embedded attachment still missing the active code route being the
case in point. Its bytes were charged twice against the one total-size allowance
while the merge deduplicates afterwards, so a different candidate that fits could
be dropped and its tool then ran without it. Shared files are counted once.
2026-08-31 21:13:24 -04:00
Danny Avila
01810098c0 🧮 fix: Judge code references by route and a declined embedding as a failure
Provisioning eligibility still asked whether a file had any sandbox reference at
all. A file provisioned to one deployment was therefore treated as done on
another, while priming resolves only the active route and left the sandbox call
without it. The database query learned about routes in an earlier commit but this
in-memory judgement did not, so even a freshly hydrated candidate was discarded.
Eligibility now asks whether a reference exists for the route this turn executes
on. It also no longer defers to the processed flag, because the pre-categorization
pass marks such a file processed on the strength of the reference it does have.

The vector service resolves with embedded false when the store declines a file,
which means the vectors are absent exactly as a thrown error does. Only rejections
were counted as failures, so the file was dropped from the queue and search
proceeded without it. A declined embedding is now a failure like any other.

One existing test asserted no provisioning state for a file whose only reference
named a stateful route, on a call that defaulted to the default route. Under the
route-aware rule that file must be provisioned again, so the test now names the
matching route, which preserves what it was actually checking: that probing the
default Code API never clears another route's reference.
2026-08-31 20:30:35 -04:00
Danny Avila
60f0d46c47 🚦 fix: Refuse uploads nothing can read, and move boot diagnostics into packages/api
Legacy DOC was on the text-recovery list but no parser handles it, so a .doc on a
deployment without RAG fell through to the native text reader and was decoded as
UTF-8. Removed, so it routes to none like the other unparsable formats.

A file kept off the model path reaches the conversation only through a file tool.
Unified mode accepted one for an agent with neither code execution nor file
search, leaving it visible in the composer while nothing could read it and the
model answered as though it were available. Such uploads are now refused with a
message naming the tools that would make them usable. The refusal is limited to
types nothing can extract: an administrator who routes a readable type to none
has chosen tool-only access deliberately and is warned about it at boot.

The agent's tools come from the read the route already performs for endpoint
resolution, rather than a second one, and are passed down like the endpoint.

The boot-time delivery-path warnings moved from the legacy entry point into
packages/api, leaving the call behind. Two nested traversals and a warning policy
are backend logic, not wiring.
2026-08-31 19:58:48 -04:00
Danny Avila
dc280c0e3e 🗂️ fix: Track embeddings per namespace and keep unparsable files off the text path
Vectors are stored per entity, but embedding state was a single record-wide flag.
Duplicating an agent copies its context file ids, so one File record is used by
two agent identities: the first embeds under its own entity and sets the flag,
and the second is then judged already embedded while searching a namespace that
never received the vectors. It also registered the file as searchable there, so
the search returned nothing rather than failing. Both the eligibility check and
the categorization now ask whether this agent's namespace holds the file, and
provisioning records the namespace it wrote through an additive update.

Records embedded before namespaces were tracked carry no entity list and cannot
say which agent holds their vectors. They are re-embedded once for the agent that
next uses them and carry the namespace afterwards. That is a one-time cost per
agent context file, accepted because a silently empty search is worse than a
re-embed.

The earlier fix for unparsable media only covered video. Archives, tarballs and
columnar data files reach the text fallback rather than the capability gate, and
the default text matcher accepts any well-formed type, so a zip was decoded as
UTF-8 into the prompt. The rule is now an allowlist of types some step can
actually turn into text, applied to the system default alone so an explicit
configuration still wins.

Upload validation resolved the agent's provider but kept the request's endpoint
type, and endpoint type takes precedence, so the Agents policy still governed
whenever one was configured. The override now replaces both or neither.
2026-08-31 19:29:05 -04:00
Danny Avila
b43acf91a4 🛃 fix: Screen persistent agent files under both current policies
Persistent agent context files are read inside primeResources, after the caller
has screened this turn's other files, so neither of the caller's checks reached
them.

Content policy was one of those checks. Current and deferred files are inspected
before their bytes can be sent anywhere; a persistent file was not, so a policy
that started refusing it after it was attached did not stop it being uploaded to
the Code API or RAG on the next tool call. It is now inspected the same way and
dropped rather than failing the turn, matching the deferred candidates, since it
was not attached by this request.

The endpoint total-size allowance was the other. Filtering the persistent set
from zero let a current attachment and a persistent file that each fit alone
exceed the limit once merged, which is the same defect the earlier two-set fix
addressed and the reason a per-request budget is the right unit. All three sets
now draw on one allowance. An earlier reply argued these files sit outside the
request's budget; that was wrong, because they are merged into the same delivery
and provisioning sets the limit exists to bound.
2026-08-31 19:09:12 -04:00
Danny Avila
a6ddebb0af 🧭 fix: Move upload routing into TypeScript and keep unplayable media off the model path
Three fixes, one of them structural.

Upload routing was a 133-line JavaScript module in /api, which the workspace
rules reserve for thin wrappers over TypeScript in packages/api. The pure
delivery-path resolution now sits in data-provider beside the resolver it
already wrapped, the agent-dependent parts sit in packages/api with the agent
read injected, and /api keeps 24 lines of wiring. Both routes resolve the
endpoint once and pass it down, so processing no longer re-resolves what
validation already decided.

An unsupported video was downgraded to text for any provider without a media
encoder. Nothing extracts text from video: speech-to-text covers audio, and the
default text matcher accepts every well-formed MIME type, so the file reached
parseText and was decoded as UTF-8 into the model context or failed a size check
on garbage. Video the provider cannot take is now kept off the model path
entirely, while remaining stored and reachable by tools. An explicit
configuration still wins, since capability gating applies to the system default
alone.

Provisioning eligibility was built from the agent's own tool list before skill
primes were resolved, so a skill contributing file_search or execute_code
through its allowed-tools produced no provisioning state and its tool then ran
against nothing. The primes resolve earlier than that set is built, so it now
includes what they contribute.
2026-08-31 18:59:46 -04:00
Danny Avila
0115f85591 🧱 fix: Apply the endpoint file policy to persistent agent context files
A saved agent's persistent context files are read inside primeResources, after
the caller has already applied the endpoint policy to the request's own files
and to the deferred provisioning candidates. Nothing filtered them, so once the
agent's provider or file configuration changed, a file the current endpoint
disables or refuses by size or MIME type could still be queued and sent to the
Code API or RAG on the next tool call.

The caller now passes its endpoint policy down, since it owns the endpoint
resolution and primeResources owns the read. Both the no-attachment turn and the
turn that also carries new attachments go through it.
2026-08-31 18:30:37 -04:00
Danny Avila
a67665d0a4 🧩 fix: Deliver provisioned files to the sandbox and validate under the right config
The central mechanism of this branch did not reach the sandbox. The graph fills
each tool call's code-session context from the sessions that exist at run start,
and buildToolCallConfig derives session_id and _injected_files from that context
alone. A file provisioned during ON_TOOL_EXECUTE was uploaded and persisted, but
nothing refreshed the context, so the first code call after a unified upload ran
against a sandbox that could not see it. Provisioning now reports the refs it
wrote and the batch folds them into every code-session-aware call, reusing the
dedupe and session-id rules the graph seed already applies.

Endpoint total-size limits were applied to the delivery set and the provisioning
candidates independently, each starting from zero, so two files that each fit
alone could exceed the limit once merged. The second pass now carries what the
first spent.

Agent uploads arrive as the agents endpoint but are processed under the agent's
own provider, and validation still ran against the agents configuration. The
provider's disabled, fileSizeLimit and supportedMimeTypes settings therefore did
not govern acceptance in either direction. The effective endpoint is resolved
before filterFile and the same one governs routing.

That resolution, the authorization check and the processing step each read the
agent separately, on a path that runs before any bytes are handled. They now
share one request-scoped read, in its own module so that mocking the file
processing service does not stub out real agent lookups.
2026-08-31 17:58:32 -04:00
Danny Avila
b43fcde69b 🚧 fix: Keep provisioning inside the boundaries the user chose
Four boundary failures, one of them introduced by the previous commit.

The conversation-wide fallback for deferred files applied whenever a thread walk
produced no ids, including when the walk ran and the branch genuinely referenced
no files. That widened an anchored branch to the whole conversation and queued a
sibling branch's attachments. The fallback now depends on there being no anchor
at all; an anchored walk keeps its own result, empty included.

Legacy mode presents an explicit upload-destination chooser, and the upload path
acts on that choice immediately, so a file carries no reference for the
destinations the user declined. Queueing on a missing reference read those
declines as pending work: a file sent to the provider was uploaded to the Code
API by the first code tool, and code or provider uploads were embedded when
search ran. Legacy mode now provisions nothing, which is what it did before this
branch existed.

Deferred discovery treated any code reference as sufficient, so a file
provisioned for one deployment was skipped on another. With resendFiles off
nothing downstream re-reads the record to notice, and the tool ran without the
attachment. Eligibility is now judged against the route the turn will execute
on, with a legacy pointer counted only when it resolves to that same route.

Unified mode removed the destination chooser and took the source chooser with
it, leaving the SharePoint picker with no trigger and its dialog permanently
closed. The composer now offers local and SharePoint sources when SharePoint is
configured, and stays a single button otherwise. Destination remains implicit on
both.
2026-08-31 17:28:06 -04:00
Danny Avila
0a83d72041 🔒 fix: Make provisioning durable before the tool that needs it loads
The shared provisioning flight covered only the remote upload. A second agent
awaiting it returned as soon as the upload resolved, while the first was still
writing the reference, and the tool loaded immediately after re-reads the stored
record and skips files whose reference is not there yet. So one parallel tool
could execute without the attachment. The flight now covers the database write,
which every waiter therefore awaits. This also removes the separate batched
persist pass, since each file persists as part of its own shared work.

Deferred candidates were only ever looked up from a thread walk, and the
Responses API always continues via previous_response_id with a null
parentMessageId, as do chat completions that send conversation_id alone. The
lookup could not run on either, so a later code or search call executed without
an attachment whose provisioning had been deferred. With no anchor there is no
branch to walk, which makes the conversation's own file refs both the correct
scope and the only one available.

Route pointers were persisted by replacing the whole metadata object, each
writer building it from its own pre-provisioning snapshot. Two agents
provisioning the same file to different deployments would drop whichever route
landed in between, causing repeated recovery uploads. A new data-schemas method
writes the single route entry through a dotted update instead, rejecting a route
key that would resolve outside its own entry.
2026-08-31 16:53:16 -04:00
Danny Avila
a1ef511945 🎯 fix: Close four routing gaps in unified upload
Endpoint overrides are documented to outrank global ones, but the two layers
were flattened into a single override map and resolution reads exact keys before
wildcards, so a global `image/png` beat an endpoint `image/*`. Merging now drops
the lower-layer entries an upper wildcard covers, which reproduces the layered
chain without changing how lookups work.

An image routed to text delivery reached neither the model nor a text context.
The images route sent it to the image pipeline, which records the routing but
never extracts text, so provider delivery excluded it and there was nothing for
the text context to add. Such uploads now take the agent upload path, which
already handles this case and either extracts the text or fails the upload with
a clear message when no extractor supports the type.

A 404 from the Code API is that API answering that the session is gone, but the
probe treated it like a timeout and kept the dead reference, so no replacement
upload was ever queued and later code tools ran without the file. Definitive
not-found responses are now separated from transient failures.

Client validation unioned the endpoint allowlist with the global text, OCR and
STT matchers in unified mode. The default text matcher accepts nearly every
valid MIME type, so an endpoint an administrator had restricted still passed
preflight and failed at upload, where the server checks the allowlist alone. The
allowlist is now the ceiling on both sides; the context tool resource keeps the
extraction-list behavior it had before this branch.
2026-08-31 16:24:50 -04:00
Danny Avila
98e1afa64f 🔁 fix: Provision each file once per request across agent contexts
Agents in a handoff or parallel graph are initialized independently over the
same request attachments, so each holds its own provisioning queue for the same
file. The first agent to run a tool uploaded and recorded it; the second still
saw an unprovisioned copy and uploaded it again, overwriting the stored
reference and orphaning the first remote object. Parallel tool calls could do
both at once.

Provisioning is now shared for the request, keyed by file, destination and
scope, while each agent still applies the result to its own tool resources. The
destination is part of the key because two agents may resolve different code
deployments, where the same file genuinely belongs in each. Failures are
dropped from the map so a later tool call retries rather than replaying the
rejection.

Adds the first spec for this callback, covering the shared path, the split by
deployment, retry after failure, and the search abort.
2026-08-31 16:15:20 -04:00
Danny Avila
460a42f38d 🐛 fix: Provision through the resolved code route and fail loudly when search cannot
Four gaps, all in code this PR introduced.

The OpenAI-compatible controllers wired the provisioning callback but not the
query that fills its queue, so deferred discovery was skipped on those API paths
and later code or search calls ran without earlier attachments.

Provisioning also hard-coded the default Code API. An agent using a configured
stateful environment had its file uploaded to the wrong deployment and
re-uploaded by priming, and a deployment whose only healthy Code API is the
stateful one could not provision at all. The resolved route now flows through
the upload and is recorded in the reference key.

Deferred candidates skipped endpoint filtering: a file the endpoint refuses by
size, MIME type, or a files-disabled setting could still reach the Code API or
RAG through provisioning after being removed from delivery. The same filter now
governs both paths.

A missing RAG_API_URL reported a benign non-embed, so the queue cleared and
file_search ran as though its input were present. It is a provisioning failure
now, and vector failures abort the turn like code failures do. An earlier commit
argued search only narrows results; that was wrong, since a search silently
omitting the file the user asked about is a wrong answer rather than a smaller
one.
2026-08-31 16:07:51 -04:00
Danny Avila
42328eab8d 🐛 fix: Discover deferred files independently of resendFiles and compose the shared icon button
Three problems, two of them mine to have caught sooner.

The thread walk was still gated on execute_code: an earlier fix for this was
applied to the wrong checkout and never reached the branch, so a file_search-only
agent still resolved no anchor and queued nothing. Either provisioning resource
now triggers the walk.

Deferred discovery also sat inside the resendFiles guard, so a conversation that
disables resending, which is about not re-sending attachments to the model, also
lost the provisioning lookup and later sandbox or search calls ran without their
inputs. Delivery queries stay gated on the setting; the deferred lookup no longer
is, since those candidates are excluded from delivery anyway.

The unified attach control also carried a copied appearance and focus recipe. It
now composes the shared IconButton with its theme size and shape variants, which
is what the styling rules ask for and what keeps future focus, disabled, and
theme behavior working without another local copy.
2026-08-31 15:42:42 -04:00
Danny Avila
16531434b1 🎨 style: Sort imports in the moved provisioning modules 2026-08-31 15:28:37 -04:00
Danny Avila
34e2ea9319 🏗️ refactor: Move provisioning into the TypeScript workspace
The provisioning service and its ON_TOOL_EXECUTE callback were 600 lines of
implementation under /api, which the workspace rules reserve for thin wrappers,
so none of it was type checked despite its TypeScript callers.

Both now live in packages/api. Storage strategies, vector upload, credential
lookup, and the file model are api-workspace concerns, so they are injected
rather than imported, and what remains on the JS side is 31 lines of wiring. The
service is built on first use for the same reason its axios instance is: this
module is reachable from the OpenAI-compatible controllers, whose suites
partially mock the package, and a load-time call would throw before any
provisioning is requested.

Tests move with the logic and improve in the process: they now exercise the real
service with injected fakes instead of mocking the package that contains it.
2026-08-31 15:20:54 -04:00
Danny Avila
0a25b9d361 🐛 fix: Select deferred files per resource and survive auth-header failures
Deferred hydration required a file to be missing both provisioning results, so a
file embedded by an earlier file_search agent was hidden from a later code-only
agent even though it has no code reference. Selection is now per requested
resource, and the caller passes which ones this agent needs.

Building Code API auth headers also happened outside the per-session try, so a
minting failure, such as a request without tenant context, rejected out of the
liveness probe and aborted initialization for any agent holding an old code
reference, whether or not a code tool would run. It is now handled like any
other failed probe: references stay unverified rather than expired.
2026-08-31 15:12:09 -04:00
Danny Avila
3a2214f83f 🔒 fix: Inspect and correctly scope deferred provisioning candidates
Three problems in the deferred-hydration path added last commit.

The thread walk that produces the anchor for deferred files was gated on
execute_code, so an agent with only file_search never resolved thread file ids
and its deferred attachments were never queued. Either provisioning resource
now triggers the walk.

Deferred candidates were passed straight to provisioning, bypassing the content
policy every hydrated file receives, so a legacy record predating the current
strict policy could have its bytes sent to the Code API or RAG uninspected. They
are now hydrated with the request's own files and inspected under the same
policy; a violator is dropped rather than failing the turn, since it was absent
entirely before deferred hydration existed.

The provisioning callback also scoped agent files by the raw batch agent id
while reading state from a fallback context, so a batch that omitted the id
uploaded agent files as user-scoped and then reconstructed them as agent-scoped
on the next turn, where the entity id no longer matched. Context and id are now
resolved together.

Credentials are no longer loaded before checking whether any attachment can
actually be probed, removing a request-startup lookup that cannot change the
outcome when every attachment is freshly uploaded.
2026-08-31 14:53:09 -04:00
Danny Avila
bf3b751d1c perf: Fetch deferred provisioning candidates in parallel
The new query ran after the existing hydration reads rather than alongside them,
adding a serial round trip to agent initialization. It is independent of all
three, so it joins the same batch.
2026-08-31 14:27:35 -04:00
Danny Avila
7132fdcf32 🐛 fix: Hydrate deferred attachments for later tool calls
An attachment accepted on one turn whose tool never ran carried neither an
embedding nor a code reference, and every hydration query matches only files
that already have one: getToolFilesByIds requires embedded, getUserCodeFiles
requires a codeEnvRef. The file was therefore absent from later turns entirely,
so asking to search or run code against it found nothing.

Hydrating it back into attachments would have re-delivered earlier uploads to
the model on every turn, so provisioning and delivery are now separate inputs:
deferred records are fetched by their own query and reach the provisioning
computation alone, never the returned attachments.

Also provisions for the host create_file tool, whose name is distinct from
write_file, and falls back to the primary context when a tool batch omits its
agent id, matching what the tool loaders already do.
2026-08-31 14:22:50 -04:00
Danny Avila
9bc01f40e9 🐛 fix: Never queue text-source records for provisioning
Legacy context files keep their content in the database with no backing object,
which the download route reconstructs from the stored text. Queueing them sent
them to a storage stream that correctly refuses the source, and since a code
provisioning failure now aborts the preflight, an agent holding one could not
run code at all. They are skipped rather than queued; provisioning them from
their stored text needs a text re-fetch and is tracked as follow-up work.
2026-08-31 11:54:20 -04:00
Danny Avila
3f15250f6b 🐛 fix: Recognize text delivery as transcript provenance
Canonical inspection treated only source 'text' as evidence that a file's text
is its own extracted content, but unified uploads persist extracted text
alongside the backing storage source, so a successfully transcribed audio file
carried source 'local' and read as having no transcript. On deployments that
block uninspectable transcripts it was rejected despite being transcribed. A
text delivery path now counts as the same provenance; the legacy source is
still honored.
2026-08-31 11:37:39 -04:00
Danny Avila
ed2969a27b 🎨 style: Sort imports in agent upload auth
Pre-existing drift in this file, surfaced because CI only checks files a diff
touches.
2026-08-31 11:17:33 -04:00
Danny Avila
fb0f9d1764 🔒 fix: Authorize permanent agent uploads without a posted tool resource
Agent upload auth returned early whenever the request named no tool_resource,
which was safe while such an upload was rejected later. Unified mode accepts it
and promotes it to a context resource, and the resource write performs no ACL
check, so an authenticated user could add persistent context, and the context
tool, to any agent by id. Only message attachments, which belong to the
conversation rather than the agent, now skip the check.
2026-08-31 10:58:28 -04:00
Danny Avila
d6a70e6396 🐛 fix: Provision to an unauthenticated Code API
Code API auth is optional: a deployment may use a legacy key, JWT bearer
minting, or no auth at all, and uploadCodeEnvFile handles each by sending
whatever headers apply. Requiring a key or JWT to queue provisioning meant
attachments never reached execute_code on unauthenticated deployments even
though the tool itself worked. Credentials now gate only the liveness probe.
2026-08-31 10:13:11 -04:00
Danny Avila
0eb921eb1c 🔒 fix: Grant agent file scope by allowlist, not by exclusion
The scope predicate excluded two known user contexts and treated everything else
as agent-owned, so a generated image, an assistants output, or any unrecognized
context was still copied into a shared agent's sandbox where other users of that
agent could read it. Only agent setup files are agent-scoped now; an unfamiliar
context provisions per user, which fails safe.
2026-08-31 09:26:12 -04:00
Danny Avila
230d2a7908 🐛 fix: Treat failed liveness probes as unknown, not expired
A timeout or 5xx while probing a code env session left its files out of the
alive set, so the staleness path cleared a potentially live ref and forced a
re-upload the same outage would likely fail, losing the file for that turn. Only
a successful response that omits the file now marks it expired.

Also corrects TProvisionToCodeEnv, which still declared the pre-merge codeEnvRef
result while the implementation and its consumer use referenceSet, and logs
rejected provisioning persistence instead of discarding it.
2026-08-31 08:52:21 -04:00
Danny Avila
f0978e7d70 🔒 fix: Keep generated code artifacts user-scoped when re-provisioning
Code outputs are recorded with kind: 'user' at generation time, but the
provisioning scope predicate treated any context other than message_attachment
as agent-scoped. An expired artifact re-provisioned on a later turn was
therefore uploaded into the agent's shared sandbox, exposing one user's private
conversation artifact to every user of a shared agent. Both the provisioning
writer and the tool-resource reconstruction now share one predicate that treats
execute_code outputs as user-scoped alongside chat attachments.
2026-08-31 08:38:05 -04:00
Danny Avila
be3d39764c 🐛 fix: Rebuild embedded agent context files as agent-scoped file_ids
Lazily embedded agent context files store vectors under the agent entity_id,
but later turns reconstructed them under file_search.files. fileSearch marks
only file_ids as fromAgent, and only fromAgent queries send entity_id, so those
vectors became unreachable the turn after they were embedded. Agent-scoped
files now rebuild as file_ids, matching the provisioning writer; user
attachments and agent-less calls keep the existing files shape.
2026-08-31 07:56:32 -04:00
Danny Avila
9c6cb14e01 🐛 fix: Reconcile lazy code-env provisioning with execution routes
Code env pointers are deployment-local, but the liveness probe always queried
the default Code API. With the staleness repair now live, a ref belonging to a
configured stateful route would fail that probe and be cleared, re-uploading a
still-valid file to the wrong deployment. Only default-route refs take part in
the check and only they can be cleared.

Lazy provisioning also wrote a bare codeEnvRef while the eager upload path
persists through mergeCodeEnvRef. Both paths now write the same shape, so the
legacy pointer and the route-keyed map stay in sync and pointers for other
routes survive re-provisioning.

Provisioning computation moves into a helper that both return paths call, so a
turn carrying no request attachments still queues the agent's persistent
context files instead of skipping them at the early return.
2026-08-31 07:37:58 -04:00
Danny Avila
82040ea5c6 🐛 fix: Thread req into liveness checks + skip credential-less staleness probes
initializeAgent primed resources with principal only, so checkSessionsAlive
minted JWT headers from undefined and every ref older than the 6h window
failed its liveness check unauthorized, churning live sandbox files through
re-provisioning each turn. req now flows through primeResources (adopting the
canonical ~/types ServerRequest), and the staleness probe only runs when it
can actually authenticate: a legacy key, or a req to mint bearer auth from.
2026-08-30 22:33:47 -04:00
Danny Avila
6a1a67242b 🐛 fix: Provision under JWT code auth + repair stale codeEnvRef re-check
Lazy code-env provisioning was gated on a loaded LIBRECHAT_CODE_API_KEY, so
JWT-auth deployments (which mint bearer tokens via getCodeApiAuthHeaders and
need no legacy key) silently never provisioned attachments. The gate now
accepts either auth mode and checkSessionsAlive composes X-API-Key with the
minted bearer headers.

Separately, pre-categorization added every codeEnvRef file to
processedResourceFiles before the provisioning loop ran, so the staleness
branch was unreachable and expired sandbox refs were never cleared or
re-provisioned. Staleness is now repaired ahead of the processed guard,
clearing both the legacy ref and its route entry so getCodeEnvRefs cannot
resolve the dead session.
2026-08-30 21:37:12 -04:00
Danny Avila
781e6679e7 🐛 fix: Persist provisionedAt + scope lazy file_search to agent resources
Round-3 Codex follow-ups completing the round-2 fixes:
- The file schema/type dropped codeEnvRef.provisionedAt, so the liveness fast-path
  was dead after reload. Add provisionedAt to CodeEnvRef + the Mongoose subschema.
- Lazily provisioned agent-scoped file_search files were only added to
  tool_resources.file_search.files, so primeFiles treated them as user attachments
  and queried without entity_id, missing the agent-scoped vectors. Add agent-scoped
  files to file_ids instead so they are queried with entity_id.
2026-08-30 20:33:55 -04:00
Danny Avila
59d46a8a3b 🧪 test: reconcile specs + lint with dev after rebase
- guard optional primeResources attachments (result.attachments?.map)
- revert 4 attachments assertions toEqual([]) -> toBeUndefined() to match
  dev's empty/no-attachments return contract (the PR's earlier
  toEqual([]) change predated dev returning undefined for those paths)
- drop unused TDefaultLLMDeliveryPathConfig import in file-config.spec.ts
- prettier formatting in resolve-llm-delivery-path.spec.ts
2026-08-30 20:12:09 -04:00
Danny Avila
e621535665 🔀 chore: align lazy provisioning with codeEnvRef schema
Rebase onto current dev brought in the metadata.fileIdentifier →
metadata.codeEnvRef migration (HEAD uploadCodeEnvFile now returns
{ storage_session_id, file_id } and requires kind/id). Update the
unified-upload code paths to match:

- provision.js: provisionToCodeEnv now derives kind/id from entity_id,
  calls uploadCodeEnvFile with the new signature, and returns codeEnvRef
- checkSessionsAlive/checkCodeEnvFileAlive: read storage_session_id and
  remote file_id from metadata.codeEnvRef instead of parsing the legacy
  fileIdentifier string
- resources.ts: primeResources gates on metadata.codeEnvRef and clears
  it on staleness; TProvisionToCodeEnv reflects the new return shape
- initialize.js: provisionFiles closure destructures codeEnvRef
- process.spec.js: align two legacyFileUploadUX tests with the
  endpoint-level check landed in 7384947 and update the execute_code
  expectation to the codeEnvRef metadata shape
- resources.test.ts: import FileSources for the typed source field and
  guard the optional attachments map
2026-08-30 20:12:09 -04:00
Atef Bellaaj
7eb0428917 🔧 feat: Unified file upload — per-mime-type routing with lazy provisioning 2026-08-30 20:12:09 -04:00
Danny Avila
b4bac55422 🔧 feat: Lazy file provisioning — defer uploads to tool invocation time
Move file provisioning from eager (at chat-request start) to lazy
(at tool invocation time via ON_TOOL_EXECUTE). Files are now only
uploaded to code env / vector DB when the LLM actually calls the
respective tool.

- resources.ts: primeResources no longer provisions; computes
  provisionState (which files need code env / vector DB uploads)
  with staleness check and single credential load
- handlers.ts: add provisionFiles callback to ToolExecuteOptions,
  called once per tool-call batch before execution
- initialize.ts: pass provisionState through InitializedAgent
- initialize.js: implement provisionFiles closure that provisions
  files in parallel, batches DB updates, clears state after use;
  store provisionState in agentToolContexts for all agent types
2026-08-30 20:12:09 -04:00
Danny Avila
6b7a45b4e1 🧹 chore: Optimize provisioning — single credential load, deferred DB writes
- Fix initialize.ts: guard provisionWarnings access with null check
  (tests don't mock primeResources warnings field)
- Fix resources.test.ts: update 4 assertions from toBeUndefined() to
  toEqual([]) — primeResources now always returns an array for attachments
  which is more consistent and avoids null checks downstream
2026-08-30 20:12:09 -04:00
Danny Avila
1201de11e2 🧹 chore: Optimize provisioning — single credential load, deferred DB writes
- loadCodeApiKey: load CODE_API_KEY once per request, pass to both
  checkSessionsAlive and provisionToCodeEnv (was N+1 lookups)
- provisionToCodeEnv/provisionToVectorDB now return fileUpdate objects
  instead of writing to DB immediately
- primeResources batches all DB updates via Promise.allSettled after
  provisioning completes
- Remove updateFile import from provision.js (no longer writes directly)
2026-08-30 20:12:09 -04:00
Danny Avila
1f72ea28bf 🔧 feat: Unified file experience — schema, deferred upload, lazy provisioning
Phase 2 fixes for the unified file experience:

- Add code env file staleness detection via batch session checks
  (checkSessionsAlive) — groups files by session_id, one API call per
  session, skips files updated within 6h safe window
- Parallelize file provisioning across files using Promise.allSettled
- Surface provisioning failures as warnings on InitializedAgent
- Fix temp file path safety (use file_id + extension, not raw filename)
- Fix inconsistent return types (normalize to [] instead of undefined)
- Wire checkSessionsAlive through initialize.js → initialize.ts →
  primeResources
2026-08-30 20:12:08 -04:00
Danny Avila
af164ae7ce 🔧 feat: Unified file experience — schema, deferred upload, lazy provisioning
Introduces the foundation for a unified file upload experience where users
upload files once without choosing a tool_resource upfront. Files are stored
in the configured storage strategy and lazily provisioned to tool environments
(execute_code, file_search) at chat-request time based on agent capabilities.

Phase 1 - Schema + Server-Side Unified Upload:
- Add FileInteractionMode enum (text/provider/deferred/legacy) to fileConfigSchema
- Add defaultFileInteraction field to EndpointFileConfig and FileConfig types
- Update mergeFileConfig/mergeWithDefault to propagate the new field
- Modify processAgentFileUpload to support uploads without tool_resource
  using effectiveToolResource resolved from config (default: deferred)

Phase 2 - Lazy Provisioning + Multi-Resource Support:
- Create provision.js with provisionToCodeEnv and provisionToVectorDB
- Extend primeResources with lazy provisioning step that provisions
  deferred files to enabled tool environments at chat-request start
- Remove early returns in categorizeFileForToolResources so files can
  exist in multiple tool_resources simultaneously
- Wire provisioning callbacks through initializeAgent dependency injection
2026-08-30 20:12:08 -04:00
Danny Avila
1e4cae07c7
🧠 feat: Retain Subagent Reasoning Like Main Chat and Drop the Running Status Chip (#15379)
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-08-30 19:33:31 -04:00
Danny Avila
fcae1025c0
🧳 feat: Register Principal-Owned Code Environments (#15365)
* feat: add principal-owned code environments

* fix: address code environment CI coverage

* fix: expose principal code environments in endpoint config

* fix: reject missing code environment bodies

* fix: harden code environment lifecycle

* fix: revalidate principal code environments

* fix: synchronize code environment authorization

* fix: bind code environments to current principals

* fix: fail closed on code ACL cache errors

* fix: prevent code environment override shadowing

* fix: preserve principal environment defaults

* fix: suppress revoked code environment aliases

* fix: fail closed on environment augmentation

* fix: narrow code environment defaults

* fix: isolate code environment fallback

* style: sort code config imports
2026-08-30 19:33:19 -04:00
Danny Avila
a9ccac8656
🧲 feat: Enable Secure Attached Environment Pairing (#15355)
* feat: add secure code environment pairing

* fix: satisfy code environment type checks

* fix: secure code environment administration

* fix: isolate code pairing control plane

* fix: validate code pairing control responses

* fix: secure code pairing transport

* fix: validate code pairing wire format

* fix: harden pairing secret lookup
2026-08-30 17:12:20 -04:00
Danny Avila
7533d138fa
🧬 perf: Evolve Compaction Guidance on Warm Turns (#15371)
* perf: evolve compaction guidance on warm turns

* style: sort compaction adapter imports
2026-08-30 17:11:20 -04:00
Danny Avila
9dcef360e2
🧳 fix: Carry Stateful Environments Through Runtime Config (#15374) 2026-08-30 17:08:24 -04:00
Danny Avila
29b3e2ef3e
📜 fix: Resolve MCP Server Instructions for Startup-Deferred Servers (#15361)
* fix: Fetch MCP Instructions from the First Live Connection

Startup inspection intentionally defers servers that need per-user or runtime context, including OAuth/OBO, custom variables, user API keys, runtime placeholders, and startup-disabled servers. An enabled serverInstructions declaration therefore never resolves to text during inspection, even though the first live connection already has the instructions from the initialize response.

Backfill resolvedInstructions from that connection through an identity-preserving YAML cache patch. Preserve updatedAt so live connections do not become stale, and globally invalidate the tenant-scoped read-through caches because YAML entries are shared across tenants. Literal instruction strings continue to win.

Scope remains YAML-tier servers. Config-overlay servers are keyed by config hash, and DB-backed user servers need a separate identity-preserving write through mongoose timestamps and credential sanitization.

* fix: Surface per-identity MCP instruction divergence

`resolvedInstructions` is a single field on a config shared by every user
of the server, and for a startup-deferred server the text now comes from
one user's authenticated connection. That is exact for a server
advertising one static block, but a server that tailors instructions per
identity cannot be represented by it.

Rather than let the stored copy churn per connection — each write
invalidates the read-through cache globally, and the model context would
vary by whoever connected last — keep the first text and log the
divergence, so the assumption is diagnosable instead of silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* perf: Skip MCP instruction backfill for non-YAML tiers

`setResolvedInstructions` writes only the YAML tier, so a config-overlay,
user, or plugin server reached it, spent a cache round-trip — a network
hop under Redis — and was refused. That repeated on every connection
creation, because the refusal leaves `resolvedInstructions` unset and
nothing memoizes the outcome.

Gate on the existing `isUserSourced`/`isPluginSourced` predicates plus an
explicit `config` check. An unset source still proceeds: it predates
per-tier stamping and the registry resolves it by name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* test: Pin the MCP instruction context read path

Every existing assertion read back through `getServerConfig`, but
`MCPManager.getInstructions` resolves instructions from
`getAllServerConfigs`, which is served by a different read-through cache.
A backfill that invalidated only the per-server cache would pass the
suite and still leave the reported bug unfixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Refuse MCP instruction backfill from mismatched configs

Self-review findings on the backfill, both in the shared-copy write:

A config-tier override shadowing a YAML base keeps the base's 'yaml'
source tag (`overlaySource`), so the connection manager's tier guard
cannot see it, and instructions fetched from a tenant's overridden
endpoint would be patched into the shared global YAML entry — reaching
every other tenant's model context and persisting after the override is
removed. `setResolvedInstructions` now takes the config the delivering
connection was created from and compares it field-wise against the
stored entry over ADMIN_CONFIGURABLE_FIELDS, refusing on mismatch.
Field-wise rather than whole-object, since inspector-derived fields
legitimately differ.

The skip condition also only refused *identical* text, so a connection
built from a stale read-through snapshot (resolvedInstructions still
unset) could overwrite already-stored different text — violating the
documented first-write-wins invariant and re-triggering global cache
invalidation per divergence. The condition is now `!= null`.

Documented the aggregate-key cross-instance write race alongside its
existing tolerance for `reinspectServer`: the backfill patch fires at
most once per server per registry lifetime, and the atomic-write
upgrade (hash fields or Lua CAS) is the follow-up that closes the
race for every writer at once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Scope deferred MCP instructions safely

* fix: Narrow optional Keyv namespace in Redis store detection

Keyv types `namespace` as `string | undefined`, so passing it straight
into `FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(...)` fails
`tsc --noEmit` in both cache classes — tsdown builds do not catch it,
but the TypeScript type checks CI job runs tsc and would. An unset
namespace (never the case after construction) now reads as not
Redis-backed, which falls back to the guarded non-Lua path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Harden the shared-instruction gate and CAS the patch

Codex round two, both verified before fixing:

A configured `oauth` block slips the backfill gate whenever
`requiresOAuth` is not literally true. The inspector stamps
`requiresOAuth = false` on every `startup: false` server without
consulting `oauth`, so the stamped population connects bare and fetches
anonymously — but the gate's safety rested entirely on that stamp: a
config reaching the manager unstamped gets OAuth machinery armed
(`isOAuthServer` treats `oauth != null` as OAuth) while
`requiresUserScopedConnection` waves it through. The gate now rejects
`oauth`/`oauth_headers` outright; genuinely static servers carry
neither.

The registry validates config identity against a snapshot that can lag
by the cache TTL, while the Lua patch checked only that
`resolvedInstructions` was unset — so a replica could validate against
an old entry, another replica replace it, and the patch land
instructions on the replacement. `patch` now takes the validated
entry's `updatedAt` and both Lua scripts (and the in-memory and
fallback paths) refuse when the stored entry no longer matches:
identity validation and the write are one compare-and-set.

Both guards verified red-without-fix; suite 36/36.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Loosen apiKey on the scoping config and sort imports

CI caught two things local gates filtered past:

`UserScopedConnectionConfig` gained `apiKey` on the strict Pick side,
but raw (pre-inspection) configs carry an optional `apiKey.source` —
exactly what the type's loosened intersection exists for — so
`agents/initialize.ts` stopped compiling. The gate only reads
`apiKey?.source`, so the loosened shape is sufficient and the
TypeScript type checks job goes green again.

The `canBackfillSharedServerInstructions` import landed unsorted in
UserConnectionManager.ts, failing the changed-file import-sort gate.

Verified with a full `tsc --noEmit` error-list diff against clean dev
(zero branch-only errors) rather than per-directory counts, which is
how the initialize.ts error slipped local verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Make MCP aggregate writes atomic

* test: Fix Redis aggregate spy assertion

* fix: Reject placeholder-bearing admin keys from shared backfill

Codex round three P1, verified end-to-end before fixing: processMCPEnv
injects an admin `apiKey.key` into the request headers (env.ts:448)
BEFORE header values get per-user placeholder resolution (env.ts:478),
so a key like `{{LIBRECHAT_OPENID_ACCESS_TOKEN}}` makes the connection
identity-scoped — while `placeholderBearingFields` never inspects
`apiKey.key` and the gate rejected only `source: 'user'`. Instructions
fetched under one user's identity could then be stored for everyone.

The gate now scans the admin key value with the same runtime-placeholder
predicate. Kept narrow deliberately: widening
`placeholderBearingFields` itself would change
`requiresUserScopedConnection` for every caller — connection pooling
included — which is its own decision.

Static admin keys still backfill (positive control test); both new
refusal tests verified red without the gate change. Suite 39/39.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* refactor: Drop gate term covered by placeholder-bearing apiKey

eb117d1b4 added `apiKey.key` to `placeholderBearingFields`, so
`requiresUserScopedConnection` now rejects placeholder-bearing admin
keys for every caller — connection pooling included — and the explicit
scan in `canBackfillSharedServerInstructions` from the rebased
32d598692 became a duplicate of that broader check. The refusal tests
stay green through the shared path alone, which also confirms the
broader mechanism covers the round-three finding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SxKWxwqxAGckYpRsYTqx3F

* fix: Preserve empty arrays in Redis aggregate mutations

* fix: Scope env-expanded MCP placeholders

* test: Harden Redis empty-array preservation

* style: Fix Redis cache static checks

* test: Narrow Redis empty-array fixtures

---------

Co-authored-by: Simon Guldager <sg@nobly.dk>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-30 16:17:26 -04:00
Danny Avila
91cfd04f22
🏕️ fix: Restore Code Environments From Runtime Config (#15373) 2026-08-30 16:15:47 -04:00