Commit Graph
8916 Commits
Author SHA1 Message Date
Ettore Di Giacinto 0c08e17a30 feat(addons): add an execution descriptor to the tool contract
A tool that shadows bash can now declare an execution descriptor: its
class, a title and a note for the model, and the skill kinds whose
runtime it supplies. The host reads it to build its text and to decide
which skills need isolated execution, instead of knowing add-on IDs.

The field is optional and needs manifest API 2. The host now speaks
API 2 and still accepts add-ons that declare only API 1, so manifests
that declare nothing new keep working unchanged.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 10:07:46 +00:00
Ettore Di Giacinto 532e3a4272 fix(worker): stop backends without Free on model.unload
model.unload calls Free over gRPC and keeps the backend process alive.
llama-cpp does not implement Free, so the call answers Unimplemented,
nothing is released, and the next backend.install for the same replica
reuses the live process. The second LoadModel on that process dies a
second after the slots are initialized, and the controller sees EOF or
Unavailable until the following install starts a fresh process.

When Free answers Unimplemented, stop the process after the unload so
the next install starts a clean one. Backends that implement Free keep
their process, so reloads stay cheap for them.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:40:46 +00:00
Ettore Di Giacinto 5dc9ed707d fix(nodes): back off failed replica scale-ups per model
When a scale-up failed, the reconciler tried again on every busy or
pressure tick, which logged a warning and sent a new backend install each
time. The "attempt" field in that warning was the loop index, so it always
read 1 and hid the repeats. A model that no request has loaded yet logged
"no load info" at warn level on every tick.

Track consecutive scale-up failures per model in memory. After a failure,
wait 30s, doubling on each further failure up to 10 minutes, before the
next try. A successful scale-up resets the count. Log the failure count
and the wait. Mark the missing load record with ErrNoLoadInfo and log it
at debug level, because only a first request can fix it.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:33:41 +00:00
Ettore Di Giacinto 2e5f6fe4f1 docs(chats): describe attach_file and send_file for add-on authors
Say that the agent can attach an existing file, what limits apply, and
that the host sends every file through the send_file op so an add-on needs
no code for it. Ask add-ons to return a short plain message when the
platform refuses a file.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:33:24 +00:00
Ettore Di Giacinto 695928e393 fix(studio): tell the model how to give an existing file
The turn block of a connector chat said that files made in a turn are
attached, but not how to give a file that already exists. Name the
attach_file tool in the block. The block stays under 700 characters.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:33:24 +00:00
Ettore Di Giacinto eca78c3f97 feat(studio): let the agent attach an existing file in a connector chat
The host sent only the files a turn made or changed. A person who asked
in a messaging app for a file that already existed got nothing.

Add the attach_file tool. It is offered only in a chat that a connector
started whose binding sends files, so a chat in Studio has no such tool.
It takes a path as the file tools show it, checks the file against the
chat's access (a scoped agent reaches only its folders), the type
allowlist, the size limit and the count, and marks it. When it refuses, the
result says why in plain words so the model can tell the person.

sendFiles now sends the files the turn made, then the attached ones, each
path once and at most five in all. It reads them with the chat's access
resolved at send time. Delivery is the existing send_file op, so every
connector add-on that implements it gets the feature with no code of its
own. A failed send says "I could not send X" and adds the reason the add-on
reports, cut to one short line.

The marks are kept in the memory of the process, like the other
per-turn state of the connector manager.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:33:24 +00:00
Ettore Di Giacinto ef79f5336d fix(studio): tell the model what to do with a binary file
read_file and edit_file answered "X.docx is not a text file (43963
bytes)" and gave the model nothing to go on.

Make the error typed and let the chat's tool list add the hint. If the
chat has a skill for the file type (docx, xlsx, pptx, pdf), the error
names it. Otherwise it suggests a script run with the commands tool, with
a library for office types, when the chat can run commands. A chat with
neither is told that no tool opens the file. The skills are resolved only
when a read fails.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:33:24 +00:00
Ettore Di Giacinto a8dfbebd81 docs(addons): document context files for add-on authors
Describe how a component declares a source (scope, path, cap), the index
format, what a turn carries and the limits, and how a chat of a saved agent
reads its own notes without gaining access to the files. Add the agent
profile file to the page on making LocalAI yours.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:28:57 +00:00
Ettore Di Giacinto 9ee1407989 feat(studio): load context files and memory_read in every chat
Each turn the chat prompt gets the agent profile and the indexes of the
registered context sources, after the persona and before the skill index.
The scope of a source is derived from the chat record: the saved agent, and
the binding when a connector started the chat. The text is the same at each
turn, so the transcript does not grow, and it feeds the refresh key so the
harness restarts only when a file changes.

memory_read reads a topic next to an index as the user, outside the chat
fence. A scoped agent reads its own notes and the user notes and gains no
access to the files. The service exposes ContextSources and
SetContextWriter so other parts of the host can register a source or plug in
the save path.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:28:57 +00:00
Ettore Di Giacinto b64b35b5a4 feat(studio): add a generic loader for context files
A source declares a scope (user, agent or channel), a home-relative index
path and a byte cap. The loader reads the indexes that apply to a chat and
returns one bounded block under a fixed header, plus a key that changes with
it. Details of a topic are read next to the index; the path is built from
the registry and the chat, so the model can only name a topic.

The package only reads. A Writer interface lets a later change plug in the
save path, and CheckIndex and CheckProfile return the current entries when a
write would pass a cap, so the model merges them.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:28:57 +00:00
Ettore Di Giacinto 438e52685b fix(cli): name the P2P resource limits flag --p2p-enable-limits
kong derives a flag name from the Go field name, so the P2PEnableLimits
fields were exposed as --p-2-p-enable-limits. The docs and the help text
describe --p2p-enable-limits. Set the name on the four commands that take
it (run, explorer, and the llama.cpp and MLX P2P workers) and parse the
documented spelling in the CLI boundary test.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:21:07 +00:00
Ettore Di Giacinto 52c59004ac test(chats): clarify configured timeout success
Describe the 1.2-second case as success within its configured timeout,
not as exceeding the actual 60-second connector default.
Remove trailing whitespace from the P2P command example.

Assisted-by: nib:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:21:06 +00:00
Ettore Di Giacinto e49f456ea3 fix(p2p): bind resource limits at CLI boundaries
Pass the resource-limit choice through application and node configuration.
Keep only the exact string true enabled, including worker and explorer
entry points. Leave other legacy environment reads unchanged.

Assisted-by: nib:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

Assisted-by: Claude Code:claude-sonnet-5-5
2026-11-03 09:21:06 +00:00
Ettore Di Giacinto d01e3cc63b test(chats): avoid a misleading tool variable
Name the MCP tool explicitly so the Ginkgo-only lint rule does not
mistake its Run method for testing.T.Run.

Assisted-by: nib:gpt-6.1-sol
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:21:06 +00:00
leilei3167 c1f697b472 fix(gallery): render single-brace JSON in Llama 3.1 function examples
The function templates stored the few-shot JSON sample inside a Go raw
string that still had an extra pair of braces. text/template printed
that literal, so the prompt showed {{"example_name": "example_value"}}
and models copied invalid JSON.

Fixes #12890

Signed-off-by: leilei3167 <imleilei123@gmail.com>
(cherry picked from commit 72f1dc25fc)
2026-11-03 09:21:06 +00:00
Vivek1898 1d55d7113f fix(p2p): read LOCALAI_P2P_ENABLE_LIMITS as documented
Root cause: #5889 changed the comparison to `!= "true"` while the result
still feeds ResourceLimit.Enable directly, so the libp2p resource manager
was enabled when the variable was unset and disabled when it was set to
"true" -- the opposite of the docs, and of the debug example that the same
PR added.

Extract newNodeConfig from newNodeOpts so the environment parsing can be
covered by a test.

Fixes #13031

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Vivek1898 <viveksingh27795@gmail.com>
(cherry picked from commit 404f86194c)
2026-11-03 09:21:06 +00:00
Vivek1898 87b37b9cb8 fix(p2p): print the p2p-worker command, not the pre-v4 worker one
Root cause: #9124 moved the llama.cpp P2P worker under `p2p-worker` and
made `worker` the generic distributed worker, but the hint printed after
`run --p2p`, the Swarm page's command builder and the docs kept the old
`local-ai worker p2p-llama-cpp-rpc`, which the parser now rejects with
"unexpected argument p2p-llama-cpp-rpc". The MLX shard command had the
same stale prefix.

The hint becomes a constant that a test parses against the real CLI
tree, so the printed command cannot drift from the command set again.

Fixes #13032

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Vivek1898 <viveksingh27795@gmail.com>
(cherry picked from commit 3c11ab96a0)

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:21:06 +00:00
Stefan Walcz 650b2c7acd fix(chats): give MCP connector tool calls the MCP tool timeout
A tool call through an MCP connector was cut after the connector
package default of 60 seconds, while the model's own MCP servers allow
360 seconds. Research and document tools behind a connector take
minutes, so a saved agent lost them mid-turn with "connection closed:
calling tools/call: client is closing" and the model fell back to a
weaker tool.

The connector service now gets the MCP tool timeout the rest of
LocalAI uses (DistributedConfig.MCPToolTimeoutOrDefault, 360 s by
default). The package default and the per-call output cap are
unchanged.

Tests in core/application start the connector service on JSON stores
against a slow MCP server: the timeout defaults to the MCP tool
timeout, a tool slower than the old cap but within the timeout
returns, and a tool past the timeout is still stopped.

Docs: chats-connectors.md, "How chats use them".

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 99f663a777)
2026-11-03 09:17:42 +00:00
Stefan Walcz dcd37a10ef fix(xsysinfo): read AMD GPU memory from sysfs when rocm-smi is missing
getAMDGPUMemory only asked rocm-smi. ROCm 7+ images no longer ship it
(only amd-smi, whose Python module is missing in the image), so on an
AMD Strix Halo (gfx1151, ROCm 10.1 image) GET /api/resources returned
"gpus": null and aggregate.gpu_count 0, and the Models page said
"No GPU found. Fit is measured against 122.7 GB of memory" while
inference ran on the GPU.

The amdgpu driver exposes the same figures in sysfs, readable inside the
container:

  /sys/class/drm/card0/device/vendor              0x1002
  /sys/class/drm/card0/device/mem_info_vram_total 536870912
  /sys/class/drm/card0/device/mem_info_vram_used  240300032
  /sys/class/drm/card0/device/mem_info_gtt_total  128849018880
  /sys/class/drm/card0/device/mem_info_gtt_used   33055911936

When rocm-smi is absent or reports nothing, walk the DRM tree with the
existing scanSysfsGPUs (cardN only, connectors skipped, vendor 0x1002)
and report every card that has mem_info_vram_total. The GPU report is
built by one helper shared with the rocm-smi parser, so the
shared-memory APU rule from #12058 (GTT >= 8x VRAM folds GTT into the
totals) applies identically. GetResourceInfo, GetGPUAggregateInfo and
the fit check all read GetGPUMemoryUsage, so they now see the GPU;
DetectGPUVendor already consulted sysfs.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit bf222cadba)
2026-11-03 09:17:42 +00:00
Ettore Di Giacinto 0b07d7fa83 fix(studio): tell the model of a connector chat that files are attached
A chat started from a messaging app has its answer read in that app, and
the host attaches the files a turn makes to it. The model did not know
this: it said it could not send files and quoted a sandbox path.

Add a short block to the turn prompt of connector chats. It names the
platform, says that files saved in the chat folder are attached after the
answer, gives the limits the host enforces, and says that files are not
sent when the binding turns them off. If the binding cannot be read the
file part is left out. Other chats are unchanged.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 09:15:50 +00:00
Ettore Di Giacinto d1edd8e961 docs(settings): say the owner checks the private egress phrase
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 644ebb9542 test(addons): inventory the new gateway and lifecycle specs
Lists the specs added by the connector error, upgrade state and wire
payload changes in the reports of the gateway, artifacts, lifecycle and
apps suites, so verify.sh finds the inventory equal to the reports. The
counts were checked against fresh Ginkgo reports of both packages.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 677e451dd8 fix(ui): do not change the request scope while rendering
AddonRegistryProvider called requests.setScope(actor) and replaced
authority.current in the render body. React may start a render and throw
it away (concurrent rendering, StrictMode), so a discarded render for a
new session epoch aborted live reads and reset the list of the actor
that is still on screen.

Render now only reads the ref: for a new actor it shows an empty list at
once. A layout effect commits the new scope and the ref. It runs at
commit, before the browser paints and before any passive effect, so no
read of the old actor can publish in between.

The hook harness of the unit tests gains useLayoutEffect and a
discarded-render mode, and a test shows that a discarded render leaves a
live read alone. The private-access form test expects the phrase in the
owner patch, as the owner now checks it.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 6ebf5383a0 test(addons): list suite cases once
suites.json kept the required cases of a Ginkgo suite twice: once at the
top level and once per report. The two copies drifted in #13034 and broke
the CI gate on master.

For a suite with reports, verify.sh now builds the required cases from the
reports, and refuses a suite that still has a top-level list. Suites with
a single report keep their one list. The redundant lists are removed from
suites.json, and prerequisites_test.sh covers the refusal.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 0c0cc77d38 refactor(addons): share the upgrade conflict and wire checks
The test for "the same pinned transition under another operation id" was
written out three times in manager.go and upgrade_operation.go, and the
"completed or failed" phase test five times. They are now sameTransition
and isActive.

OwnerHandler rejected a request that carried a second routing payload with
a hand-written list of nil checks per branch, each list slightly
different. wireRequest.exclusive counts the payloads once. The earlier
branches return, so every branch rejects the same requests as before.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 6a7a2db672 fix(addons): do not panic on an unknown upgrade phase
upgradeTransition ended in panic for a phase or event it did not know.
The phase of a submitted operation is read from the journal on disk, so a
corrupted file would crash a background goroutine. The reducer now
returns errInvalidUpgradeTransition, and submit marks the operation as
failed and returns ErrOperationFailed. The current call paths only pass
known phases, so this is a guard, not a fix of a known crash.

upgradeViews also grew without bound, and every persist walked it. Views
of finished upgrades are now kept for the newest 256 only; the journal
still answers for older ones. The journal itself is rewritten on each
persist, so the walk stays proportional to what is written anyway.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 564ee50df2 refactor(addons): bound a connector call once per hop
ConnectorCall, localConnectorCall and Carrier.connectorRequest each set
their own 45 second timeout. On a frontend the first and third nested
inside each other, and on the owner the first and second did. Each hop
now sets it once (connectorTimeout): the forwarding carrier on a frontend,
and localConnectorCall on the owner, which must bound the work whichever
door the call came through. ConnectorCall no longer adds a third one.

The kind lookup in localConnectorCall uses slices.IndexFunc instead of a
loop that kept scanning after a match.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 4fbfc4586b fix(addons): keep the cause of a connector failure
localConnectorCall and the kinds lookup turned every error from the
connector source into a bare ErrUnavailable. VerifyTokenWithMe treats
that as "could not tell" and saves the binding, so a definite refusal by
the add-on was indistinguishable from an outage, and the cause was lost.

An add-on refusal (invalid request, or denied for this person) now keeps
its type through Refused. Other errors still read as ErrUnavailable but
wrap the cause, so errors.Is and logs see it. The forwarding carrier maps
the owner's 400 back to ErrInvalidRequest instead of ErrUnavailable.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 5295c932c0 fix(addons): check the private egress phrase on the owner
With add-ons enabled /api/settings rejects chats_egress_allow_private, so
the typed phrase check there never ran on the only write path, the
host-settings-update action. The page asked for the phrase but dropped it
from the patch, and a direct admin call could turn the switch on without
it.

The patch now carries chats_egress_private_confirm and hostsettings.apply
rejects turning the switch on without it. The phrase is hashed into the
operation receipt like the rest of the patch and is never stored. The e2e
fixture enforces the same rule and the egress spec asserts the phrase in
the owner patch again.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:45:52 +00:00
Ettore Di Giacinto 022e33f5d6 docs(addons): document add-on roles, distributed owner, delivery and Sandbox isolation
Add a table of what runs in the combined, owner and frontend roles, and
explain that an owner must join distributed mode with the frontends.
Describe pull and push connector delivery, the one-connection-per-Slack-app
rule and the reinstall needed for new Slack scopes. State what the Sandbox
needs to report an isolated backend, and that gVisor needs no /dev/kvm.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:41:10 +00:00
Ettore Di Giacinto 7e14a26a15 feat(addons): expose the shared model picker to add-ons
sdk.ui gains ModelPicker (multiple, allowCustom, clearable, field
variant, alias tags). HomeModelPicker and ModelSelector stay as names for
the same picker so existing add-ons keep working. The add-ons guide
lists the new component and its props.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:07 +00:00
Ettore Di Giacinto 6ffc9e82c1 feat(ui): use the shared model picker everywhere
Settings, config fields, the failover and router editors, the agent task
form, the scheduling rule sheet and the agent job form now use
ModelPicker. SearchableModelSelect, ModelMultiSelect and the scheduling
ModelPicker are removed; ModelSelector stays as the name add-ons use and
draws the shared picker. Aliases come from the capabilities listing, so
the scheduling sheet no longer builds its own hints.

A broken alias is listed, marked and not selectable. The picker's style
sheet now loads with the picker, so the e2e specs that read a settings
group right after opening it wait for its first field.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:07 +00:00
Ettore Di Giacinto dbb8e8a022 feat(ui): make the home model picker the shared model picker
Move HomeModelPicker to components/ModelPicker and give it what the
other pickers had: multiple selection with chips, free text, a clear
button, a search box on demand, a form-field look, and loading and
empty states. Every entry shows 'alias of X', disabled, pinned or
broken-alias tags from the capabilities listing, and entries that
cannot be used are not selectable. Auto-select skips them.

Add-ons keep HomeModelPicker as a name for the same component.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:07 +00:00
Ettore Di Giacinto 688b12a038 feat(ui): resolve alias models fully in the capabilities listing
An alias entry now takes its capabilities, backend, MCP, voice cloning,
upscale scale, 3D operations and loaded state from the model it points
to, and keeps its own name, disabled and pinned state. An alias that
cannot be resolved (unknown target or a chain) is logged at warn and
listed with alias_of and alias_error instead of vanishing.

The chat model checker refuses a broken alias when it is saved.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:07 +00:00
Ettore Di Giacinto fc087c13e8 fix(ui): style the host controls of an add-on like the Settings page
The controls sit in a fieldset, which drew the browser border and
padding around the rows. The rows also showed raw setting keys such as
chats_egress_ceiling next to their labels.

Reset the fieldset, hide the raw keys in the add-on controls, and make
the row label fall back to the key in words, so a snake_case key is never
a label. Add e2e and unit tests that fail when a raw key is drawn.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:00 +00:00
Ettore Di Giacinto 4eab1148be fix(ui): keep the chat following the bottom after a send
A smooth scroll to the bottom from far up reports a distance above the
pin threshold on its way down. The scroll handler read that as the reader
scrolling up and unpinned the thread, so the new message and the answer
that streamed in after it stayed out of view.

Only a move up unpins the thread now. A reader who scrolls up while an
answer streams is still left where they are.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:40:00 +00:00
Ettore Di Giacinto 9fafea8fdf fix(chats): show the network switcher and say why execution status is unknown
The Network row of the Tools menu could never show the Off, Registries and
All switcher: the host stopped setting `applies` because isolation alone does
not prove network-policy enforcement, and nothing else set it.

Let the provider report enforcement in its typed execution status
(`networkPolicyEnforced`). The host sets `applies` only when that report is
true and the status is available and isolated. The Sandbox reports it for the
gVisor and KVM backends, whose guests reach the network only through the proxy
that asks the host. Providers that do not report it keep the "not enforced"
text.

When the host cannot read the status, give the unknown state a reason
(`unknown_reason`) in the API and the Tools menu: owner unreachable, not
reported, stale, invalid, not selected, no answer or reported unknown. Log the
class of each failure, rate limited. After a failure that a restart of the
add-on owner can cause, let a long turn bind and read again, at most once every
five seconds, instead of keeping the first failure for the whole turn.

The Sandbox now reports "unavailable" with a reason when it started without
settings, and explains a backend it does not know.

When the Sandbox document skills are hidden because the Sandbox is not known to
be isolated, say so in the skill list, in the turn note and in the
`load_skill` error, instead of "This chat has no skills now".

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:30 +00:00
Ettore Di Giacinto 024b7196de fix(mcpconns): guard the dial when private addresses are allowed
With mcp_connectors_allow_private on, a connector used a plain transport
with the environment proxy and no address check, so its URL could reach
cloud metadata addresses. Build the private path on the safehttp guard
with private addresses allowed: it resolves once, checks every answer,
and keeps the metadata addresses blocked. An optional DenyAddrs hook
refuses further addresses, for example the server's own.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:12 +00:00
Ettore Di Giacinto 59e3f01739 fix(safehttp): block more cloud metadata addresses and NAT64
Add the Azure wire server (168.63.129.16), Oracle (192.0.0.192) and
Alibaba (100.100.100.200) addresses to the metadata list. The Azure one
is public, so web_fetch reached it. Check the IPv4 address inside a
64:ff9b::/96 address too, so private addresses and metadata addresses
cannot be reached through NAT64 when private addresses are allowed.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:12 +00:00
Ettore Di Giacinto 1b90305125 fix(sandbox): cap the default bash timeout at the maximum
A call with no timeoutSeconds ran for the 120 second default even when
an administrator set a lower maximum. Use the smaller of the two.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:12 +00:00
Ettore Di Giacinto e06a6244c8 fix(sandbox): clean up and back off after a failed idle suspend
The sweep only logged a failed suspend and tried again every minute,
holding the entry for up to a minute each time. When the guest was gone
the entry kept pointing at the dead sandbox, so the chat's bash failed
until the sandbox reached its maximum age.

Drop the sandbox on ErrGone so the next call makes a fresh one. For any
other error, wait longer after each failure (two minutes, doubling to
thirty) and reset the wait on the next call. The log now names the user
and conversation.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:12 +00:00
Ettore Di Giacinto d1aa963ba1 fix(studio): refuse a host command too long for its approval card
The approval card cut the command to 2000 bytes while the full text ran,
so a prompt-injected model could hide a tail behind harmless text. Refuse
the call with a message instead, so what the person allows is what runs.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:39:12 +00:00
Ettore Di Giacinto 64e0ecd3f8 fix(studio): wake the waiter of a sub-agent whose turn was reaped
A parent waits on a channel that only the end of the child turn on the
same process fills. When the reaper ended the child turn as dead, nothing
filled it, so a background report blocked until the server stopped.
ReapStale now reports an interrupted status to the local waiters.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto 2f03946238 fix(studio): bound the model call of a compaction
A compaction ran on a context with no deadline, so a model that never
answered held the chat busy for good. Give it a 10 minute limit.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto d1954be534 fix(studio): read chats without taking a write lock
GetChat of the SQL store opened a write transaction with a no-op update
on every read, and the JSON store saved the record on every read, only
to rewrite the legacy builtin:docx style markers to addon:docx.

Rewrite the markers once when the store opens, in an idempotent step that
changes a row only if it still holds the value it read. A read now
normalizes the markers in memory and never writes. A failed rewrite is
logged and does not stop the store.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto 8079414946 fix(studio): cut sub-agent titles on a rune boundary
The title of a sub-agent chat was cut at a byte count and could end in
the middle of a UTF-8 character. Use the rune-safe CapText. Also use the
slices package for the tool lookup and the waiter removal.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto dafaa33590 fix(studio): keep queued messages when a turn is stopped
Stop dropped the messages that waited for the next turn, although they
were in the transcript already, so the person saw them but the model
never did. They now stay queued and go to the model, ahead of the next
message.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto 55ad3d0b28 fix(studio): cap the messages that wait for the next turn
The queue of messages that wait while a turn ends or the harness swaps
had no limit. Cap it at 32 messages and 256 KiB, and refuse a message
over the cap with ErrTurnRunning before it reaches the transcript.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto 8ff3fb8780 fix(studio): do not refuse a message that is already in the transcript
When a turn was stopped between Send logging a message and queuing it,
Send returned an error although the message was in the transcript, so a
client retry logged it twice. The message now waits for the stopped turn
to end and starts the next one.

The fake harness gets an Inject hook and a step that ignores a stop, to
reach that window in a test.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00
Ettore Di Giacinto cc0ab51438 fix(studio): time turn heartbeats with the database clock
A turn beat was written with the clock of the frontend that runs the
turn and compared with the clock of the frontend that reaps. A skew of
about the turn timeout made the reaper end live turns.

ReapTurns now takes an idle duration, and the GORM store reads the time
from the database for the claim, the beat and the cutoff. The JSON store
is single-process and keeps the local clock.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 08:38:53 +00:00