Commit Graph
59 Commits
Author SHA1 Message Date
Ettore Di Giacinto 31aa996272 feat(addons): let the owner relax the confirmation of a write tool
A write tool with approval "always" always waits for a person. A tool can
now also declare approvalOverridable, and the owner can then choose not to
be asked for that one tool, with a user setting approval-<tool> (ask or
skip, default ask). It is the only way to relax the ceiling.

The host honors the setting only for the owner's own chat and for a
messaging chat whose senders are all paired with the bot. Allowlisted
but unpaired senders, anyone-mode senders, roles, triggers, jobs, pages,
sub-agents and calls without a chat still wait for a person. The setting
is read again at the apply. Every run without a card is logged with the
tool, the connection and the outcome, never the arguments, and is added
to the latest decisions of the connection. The access view and the
access preview list the relaxable tools and who they run for.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-05 10:01:46 +00:00
Ettore Di Giacinto 894692ca16 feat(github): allow classic tokens and any-repository lists, both opt-in
The GitHub add-on refused classic tokens in the page only, and its
repository list took exact owner/name entries.

A classic token (ghp_) is now an explicit choice of the connection. The
kind declares a token guard: the host refuses a token with that prefix
unless the connection sets the option classicToken to allow, and the
request that turns it on must carry confirm_wider_token. It is checked
when the token is saved and again each time it is used, so switching
the option off stops a stored token. The list and the write mode still
apply. The page shows a warning and two ticks.

The list takes owner/* and *. The host matches them before the add-on
runs, without regard to case and on whole names, so other-owner/x and
owner-evil/x are not covered by acme/*, and a key that holds a star is
never a name. A kind opts in with wildcardResources; only GitHub does,
so the other add-ons keep exact matching. Saving * needs
confirm_any_resource, and the write mode stays separate. A read tool
that declares enumerates (the new list-repos) runs only for a list of *
and asks GitHub which repositories the token reaches.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-05 09:53:17 +00:00
Ettore Di Giacinto d4c268b644 fix(connectors): create credentials-only bindings, answer 403 on denial, allow 50 bindings
Creating a binding with no agent (home-assistant, jellyfin, pikvm,
github) failed with a 500. The token check for a new binding looked up
the agent even when the binding had none, so the host refused it. The
check now looks up an agent only when the binding has one. The token is
still required and a foreign agent is still refused.

A host denial (connectors.ErrForbidden) reached the client as a 500
server_error. The connector endpoints now answer 403 with a fixed text
that names no part of the check.

A person could have 10 bindings. Several bots plus one credential
binding per service need more, so the limit is now 50, like the limit
of resources and triggers. The docs and the API description say so.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-05 09:42:54 +00:00
Ettore Di Giacinto 27556acbba feat(chats): reply language auto follows the owner's interface language
With the reply language on auto, the sentences the host writes in a chat
used the language detected from the sender, and the interface language
of the owner only when a server hook supplied it. No hook was wired, so
the interface language was never used.

The page of a connector add-on now sends its interface language with
each save (ui_language). The binding keeps it, so every frontend of a
cluster knows it. With auto, the host speaks that language when its
catalog has it, and reads the language of the sender only when the
owner's language is unknown or not in the catalog. A save without the
field keeps the stored value, and a value that is not a language tag is
ignored. A fixed reply language still wins.

The label of the Automatic option says what it does now, in all nine
languages of the page.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-05 09:25:21 +00:00
Ettore Di Giacinto 8df6954c5a feat(chats): translate capability titles and refusal texts by code
The access view, the preview and the kinds call returned English text
for each capability and each refusal. They now add the same text in the
language of the request (lang query, then Accept-Language), in new
localized_* fields. Codes, English fields and the model-facing text stay
as they were.

The texts live in the new connectortext package, one JSON file per
language (it, de, es, fr, id, ko, pt-BR, zh-CN), keyed by capability
and by refusal code. A test fails when a capability or a code lacks a
text in any language, or when {action} is lost.

The shared connector page asks for the page language and shows the
localized fields, with English as the fallback for each field.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-05 09:16:07 +00:00
Ettore Di Giacinto 3167c58351 feat(memory): call the provider of the notes from the host
The host now calls the roles of the notes slot itself, not only the tools
the model sees.

- At the start of a turn the context role is called with a 3 s limit and
  a 6 KiB cap. The text goes in front of the message as data, in markers
  that it cannot close, and not in the prompt, so it does not restart the
  chat engine. The session passes the turn message to the hook and takes
  an optional per-turn context block from it.
- A write through a provider follows the save level, the agent grant and
  the messaging rules. Only Save and tell me applies a write at once, and
  not for a remote provider that cannot undo. Other writes are held for a
  confirmation.
- The export and the forget of a provider are routed by
  GET and DELETE /api/chats/memory.
- A provider that fails three times in a row is left alone for a minute,
  the slot shows a plain notice and the chat goes on without notes.
- Every call writes one audit line with the outcome and the size, and
  never any text.
- The add-on that the built-in provider replaces is an alias: it is not
  listed and a choice that names it means the built-in provider.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-05 01:45:30 +00:00
Ettore Di GiacintoandClaude Sonnet 5.5 62d75c0de0 feat(memory): serve the notes through a provider slot
Memory is the first provider slot. The person picks one provider for the
notes: the built-in one, Off, or an add-on that declares the slot. The
choice is kept per person with the other host settings.

Core stays generic. providerslots.go lists the providers of a slot,
stores and resolves the choice, and gates an add-on that declares a slot:
its context files, skills and role tools count only while it is the
provider picked, and the chat can narrow a slot off for itself. The
notes wire into it in memory.go: an adapter gives the add-on's files and
skills behind the MemoryProvider interface, the built-in provider stops
reading while another serves, and the On or Off switch and the choice
stay one answer.

If the add-on that was picked stops, is disabled or is removed, memory is
off with the code memory_provider_unavailable and the add-on is named.
It never falls back to the built-in provider, so a note is not read or
saved where the person did not choose. The choice stays stored.

Add GET /api/chats/providers, GET and PUT /api/chats/providers/{slot}.
Settings > Memory lists the providers from that answer with a generic
picker, shows the failure with the shared Notice and offers to use the
built-in provider. Document the facility for add-on authors, and test it
with a fake add-on that provides the slot.

Assisted-by: Claude:claude-sonnet-5-5 go
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjrQyk8ovFXvy5zrjHX1BD
2026-11-05 00:59:43 +00:00
Ettore Di Giacinto adbb71cfdf feat(memory): add Always confirm, an agent save rule and a messaging row
A person can now choose four save levels on the Memory tab: Save it and
tell me, Ask me first (the default), Always confirm and Never. Always
confirm is like ask, and nothing can loosen it: no agent or messaging
setting overrides it, and the model cannot undo or forget a note by
itself (revert_change is refused, and the person uses the Undo).

One function, effectiveSaveLevel, combines the rules. The strictest wins,
in the order auto, ask, always, never. The inputs are the level of the
person, the rule of the saved agent, the messaging row and the chat.
A table test covers every combination.

A saved agent has an optional memory_save field: always or never. It can
only be stricter than the person, so auto is not offered and the API
refuses it. A write without the field keeps the stored rule, and use_mine
clears it. It is stored by both stores and is part of an agent export and
import. The agent form shows three cards under When it wants to save a
note.

The new row In messaging apps (messaging_save on GET and PUT
/api/chats/memory) takes ask (the default), paired_private or never. With
paired_private a note is saved at once only when the sender is paired and
the chat is one-to-one, and it goes to the notes of that conversation,
with an Undo. The limits stay: five notes a reply, the size limit, and a
note that looks like a secret still waits. The row cannot loosen the level
of the person when that is always or never. A setting that cannot be read
reads as ask.

The connector turn now carries whether the chat is one-to-one and whether
the sender is paired, in the turn grant and in the worker JSON. Both read
as false when unknown. The Telegram, Slack and Discord transports already
set Direct; new specs pin it for private, group and supergroup chats,
direct messages, channels, group conversations and threads.

A note from a messaging chat goes to the conversation first. It reaches the
notes of the agent or of the person only from a paired sender, only when
the memory choice of the agent allows that scope, and only after that
sender confirms it. Under Always confirm only the paired sender who asked
can confirm. An agent whose memory is None still saves nothing.

The script of the confirm-in-chat specs now proposes a note for the
conversation, because a sender who is only listed cannot write to the
notes of the person.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 19:45:08 +00:00
Ettore Di Giacinto 917b54e6c1 feat(memory): let each saved agent choose its notes
A saved agent now has a memory choice: Mine and its own, Its own only,
Mine, or None. The host enforces it. The notes in the prompt and the
scopes of context_read follow the choice, and a memory note may go only to
the scope the choice allows: the agent's own notes, or the person's. The
propose_change tool offers the model only those scopes, a note needs no
scope, and any other scope is refused. None offers no memory kind.

An agent saved before the field has no choice and behaves as before: it
reads notes when its add-ons name Memory and the model picks the scope.
A write without the field keeps the stored choice. The person's Off, a
private chat and the agent's grant still decide first. Sub-agents get no
notes and cannot save; triggers read per the choice and cannot save.
Messaging chats read as before.

The agent form has a Memory section with the four choices, an "As before"
note for older agents, a switch for the permission to save, and the line
that memory is off in the settings. New agents start with Mine and its own.
Deleting an agent that has notes asks to keep, export or delete them
(DELETE /api/chats/agents/{id}?memory=keep|export|delete). Kept notes show
on the Memory page under Deleted agents, and the notes list has a tab for
each agent. Copying an agent never copies its notes.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 18:26:20 +00:00
Ettore Di Giacinto ea1f5f449f feat(studio): let the model search and read earlier chats
A new opt-in tool group, Past chats (history), gives a chat two read-only
tools. search_chats finds messages in the earlier chats of the person
(query, scope this_chat, this_agent or all, limit, since) and read_chat
reads one chat from a message number. A call returns at most 10 hits with
300 character snippets, or 20 messages and about 6 KiB. Every result is
wrapped as untrusted text, a closing line in an old message cannot end
the wrapper, and the tool says plainly when nothing is found.

What the tools never reach: another user's chats, private chats (a chat
with the notes of memory switched off), sub-agent runs, and every chat
that no person started, which leaves out the messaging chats that hold
the words of other senders. A saved agent needs the new grant
history.read, which is off by default and turns the group on when it is
saved. An agent that works in a folder may search only its own chats. A
run that starts by itself, a messaging chat and a sub-agent do not get
the tools. The grant, the switch and the agent are checked again at each
call. Each call writes one log line with the tool, user, chat, scope, a
hash of the query and the chats returned, and never a query or a message.

The Tools menu and the agent form show the group and the grant through
the existing lists. The docs and the API notes describe both.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 18:04:50 +00:00
Ettore Di Giacinto 9422011f20 feat(connectors): write the bot's own sentences in the language of the person
The confirmations, refusals and notes that the host writes in a chat
without the model were fixed English: "Started a new chat", the question
about a proposal, the file notes, the pairing replies. They now come from
a catalog (core/services/studio/hostmsg) with a template for English,
Italian, German, Spanish, French and Portuguese (Brazil), and the same
{placeholders} in each. Text for the model (tool descriptions, the held
back note, the thread block) stays English.

A connection has a reply_language setting: auto (default), en, it, de,
es, fr or pt-BR. With auto the host scores the sender's message against
the common words of the six languages, with no model and no dependency.
A message that does not show a language ("ok", "yes", "thanks") changes
nothing, and a short message cannot move a conversation to another
language. The last clear language of a sender and of a chat is kept for
24 hours; a trigger that writes on its own uses the chat's. The setting
is stored in the JSON and the gorm store, is kept by an update that
leaves it out, and is on the connection page under "How it answers".

Every sentence goes through msg or a hostmsg.Voice. A guard test fails
when a fixed text reaches say, Post or Outgoing, when a key is not in the
catalog, or when a key is never used. The reply parser now reads the
yes, no and undo words of the six languages, which the new texts ask for.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 18:03:27 +00:00
Ettore Di Giacinto 2316e14632 feat(memory): add a Memory On/Off switch and private chats
Memory now has one switch, On or Off, in the Memory tab of the account
settings. Off means nothing from the notes is put in the prompt, the
model has no memory skill, tool or note kind, nothing is proposed or
saved (also by triggers and other runs that start by themselves), and
the model is told in one line that memory is off. The notes stay and can
still be read, exported and deleted.

A chat can be private: the Memory row of its Tools menu is the memory
switch group, stored with the chat. The narrowest level wins: chat,
agent, person, server. A saved agent cannot widen the choice of the
person. The tools list says which level is off (memory.code, blocked_by,
fix); the chat shows a chip and one line, and the agent form shows the
person's switch as a read-only line.

New API: GET and PUT /api/chats/memory for the switch and the save level,
and GET /api/chats/memory/export for a zip of the notes. The Memory tab
gains the switch, the three save levels, Export and Forget everything.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:50:26 +00:00
Ettore Di Giacinto 7df9b263ed feat(addons): carry translated text in the manifest and the library
The manifest said its text in English only. An add-on can now translate
what the host shows to people, and the library list shows it before the
add-on is installed.

The title and the description use the language maps of ui.title and
ui.description, which every host already reads. A new optional field,
i18n, translates the connector kinds by language tag. It stays under api
2 and its set of fields is closed: the title and the description of a
declared kind, and only where the English kind has them. Tool
descriptions, schemas and skills are for the model and stay English.
Tags follow the language map rule (at most 16, no English), each string
has the limit of its English field, all translated text has at most
12 000 characters, and markup, control characters and direction
overrides are refused. A host that does not know the field refuses the
manifest, as it refuses any unknown field. The field is part of the
manifest, so the package digest and the signature cover it.

The catalog generator copies the translated title and description of
the manifest into each library entry (ui.titles, ui.descriptions), the
host validates them like the manifest values, and a library search
matches the English and the translated text. The apps list and the
connector kinds return the text of every language and, when the request
names a language (lang, else Accept-Language), the resolved text beside
the English fields: language, localizedTitle and localizedDescription.
No existing field changes.

Every add-on on master now has its title and description in Italian,
German, Spanish, French, Brazilian Portuguese, Simplified Chinese,
Korean and Indonesian, and the chat connectors carry their kind text in
the same languages. The packaging tests require that, and that a
translation has the fields of the English text it translates. The names
of the add-ons that provide a role are matched in their English title
only: a translation is often an ordinary word of its language.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 17:46:53 +00:00
Ettore Di Giacinto 4d58fa0056 feat(studio): search all chat text with a real index
Search read the newest 500 chats and scanned them in memory, so older
chats dropped out silently. The chat store now keeps a search index of
the text of user and assistant messages: FTS5 with the trigram
tokenizer on SQLite, a pg_trgm GIN index on PostgreSQL, and a scan of
the same table when neither is there. The file store scans its
in-memory transcripts for up to two seconds, newest chat first, and
says when it stopped early.

The index row is written in the transaction that writes the entry and
removed in the one that deletes the chat or truncates the transcript.
A query joins the chat row for the owner, so one user never sees the
chats of another, and sub-agent chats stay out. Messages written before
the index existed are added in the background with a resumable cursor
and progress in the log; startup does not wait for it.

GET /api/chats/search keeps its fields and adds the role, time, match
count and highlight offsets of the hit, and filters for agent, origin,
role and date range. A phrase in double quotes is one word. Hits rank
by match quality and recency. group=message lists every matching line.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:45:59 +00:00
Ettore Di Giacinto 80e4c61610 feat(studio): explain the first blocker of each connector capability
The access view said only that a capability was refused, in free text.
It now reports, for every capability of every kind of sender, the first
link of the permission chain that fails: a closed code, who holds the
link (server, tools, agent, connection, sender), one plain sentence, one
fix as a kind and a stable target, and the links not yet checked. The
chain order is server setting, Tools switch, agent permission, the
permission of the connection, then sender trust. The code is produced
where Authorize and the capability switches decide, so the English
reason and the decision cannot drift apart. The Tools switch of the
agent and the server triggers setting are now switches in that chain.

POST /api/chats/connectors/{id}/access/preview runs the same chain on a
form that is not saved (id "new" works with a kind), optionally for one
kind of sender. It writes nothing, records nothing and never takes or
returns the token.

The held-back note for the model, the tool errors and the trigger tool
reason take their sentence from the same table of codes, in plain words
with no setting names.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 13:50:05 +00:00
Ettore Di Giacinto a933efeb8a test(studio): cover commands_host_exec in the add-on list and refresh swagger
Pin the field on the chat and the draft endpoint for off, ask, on, no
setting and an unknown value. Regenerate the swagger files for the new
field.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:20:31 +00:00
Ettore Di Giacinto e0d77e410c Report the mode of the built-in bash in the chat add-on list
A chat with no add-on that runs commands could not tell a person
whether the server's built-in bash was off or simply not selected.
The add-on list now carries commands_host_exec (off, ask or on), a
read-only copy of the chats_host_exec setting as it applies now.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:19:45 +00:00
Ettore Di Giacinto 3edcec1813 feat(studio): let the owner allow acts for triggers proposed from a chat
A trigger proposed from Slack or Telegram was always read only, and
the person had no way to let its runs act except to edit the trigger
on the Triggers page afterwards.

The approval of a binding now has an acts list, the act.* permissions
that a proposal from its chats may carry. It is empty by default, so
nothing changes until the owner sets it. It is stored in the same
approval JSON (no migration), checked against the closed list in the
API, and always limited to what the agent holds.

The acts of a proposal are what the model asks for, within the list of
the binding and the grants of the agent. The rest is dropped and
named in the proposal text, and the model is told which acts it may
ask for and to ask the person first when the job changes things. A
read only proposal now says what the person can do next.

For a proposal with acts, auto_readonly still asks. auto starts it at
once only when every act is allowed for the binding, nothing was
dropped and every sender of the turn is paired; otherwise it asks. A
trigger made without asking also keeps to the acts of its binding at
run time.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:17:23 +00:00
Ettore Di Giacinto 5df9c43578 fix(studio): trust a sender who is on the allowlist for capabilities that are not sensitive
A binding with its capabilities ticked still gave nothing to the
owner's own allowlisted account: Authorize wanted a pairing or a role
even for capabilities that are not sensitive, so the agent had no
reminder, memory or talk-back tool.

A sender whose ID or @name is an entry of the allowlist now holds every
capability that is not sensitive, within the ceiling of the binding and
while no switch is off. The decision says Via "allowlist". A sensitive
capability still needs a pairing, and a role never grants it.

The entry of a channel or group lets its members talk to the bot but
does not trust them: the trust is matched against the sender only, and
a binding that accepts anyone lists nobody. Such a sender still gets
capabilities from a pairing or a role. The capability gate takes the
handle of the sender for the same match, and a sensitive request
ignores the allowlist as it ignores roles.

When a tool is held back for a missing right, the turn now carries a
short note with the reason in plain words and tells the model to relay
it, instead of saying that it has no such tool. The reasons are kept
in the grant of the turn, so a worker reports the same words. The
access view lists a "listed" sender apart from one let in through a
channel.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:11:25 +00:00
Ettore Di Giacinto ad39954af1 feat(addons): let a chat enable several connections of a credentialed add-on
A chat now enables a set of connections for an add-on whose tools run with
a credential, and the model names one of them in each call. This applies to
every add-on that declares a credential in its manifest.

- The Tools menu row of the add-on lists the connections with a checkbox
  each. The saved agent names a set too (an empty set means each chat
  chooses). A chat that a connector binding started keeps that binding
  alone. With one connection, it is the set. With none enabled, the tools
  are not offered.
- With one connection enabled the tools are unchanged. With two or more,
  the host adds a required `connection` argument to every tool of the
  add-on in the definitions it gives the model: an enum of the connection
  names (the display name folded to lower case, digits and hyphens; the
  description keeps the display name). The add-on never sees the argument.
- The name is checked against the set the chat has now, not against all the
  connections of the person. A missing or unknown name returns a sentence
  that lists the allowed names, and nothing is sent. A write tool never runs
  without the name. Text in a tool result cannot widen the set, and the
  owner checks the chosen binding against the set again.
- The resource list of the named connection is the only one that applies.
  Results, the approval card and the text for a pending change name the
  connection; the audit line has the binding ID.
- A connection name must be unique per person and add-on among the
  credential-only connections, compared by name fold. Older duplicates get
  a numeric suffix in the model's list.
- The stored chat, draft and agent values move from one binding ID per
  add-on to a list of IDs. A single string is read as a set of one in the
  stores and in every request, so older data and clients keep working.
  PUT /api/chats/{id}/connections takes `connections`; the older
  `connection` field still works.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node eslint playwright swag
2026-11-03 22:33:28 +00:00
Ettore Di Giacinto c56c1ac343 feat(addons): choose the connection of a credentialed add-on explicitly
A chat never uses a connection that the host picked for it. For an add-on
whose tools run with the token of a connection (such as GitHub):

- With one connection, it is selected. With several, the Tools menu row
  of the add-on has a Connection selector, and until the person picks one
  the tools are not offered and the row says so. With none, the row says
  so and links to the Connectors page. The choice is stored with the chat
  and its draft, like the other switches, and follows a fork, a
  sub-agent and the runs of a trigger the chat proposed.
- A saved agent can name its connection per add-on (agent form field, API
  field connections). Its chats use it, show it read only and cannot
  change it. An agent with no choice follows the same rule as a chat. A
  chat that a connector binding started keeps using that binding.
- CredentialFor takes the selected binding and uses only it, so the
  resource list of that binding is the only one that applies. A call with
  no selected binding is refused with the code select_connection and a
  note the model can relay. The first-match code (connectors.Pick) is
  removed.
- Tool results and proposal cards say which connection acted, for example
  "as Work, repository acme/widgets". A proposal is applied with the
  connection it was previewed with. The credential use is logged with the
  binding ID.

Contract: the credential slot takes an optional resourceLabel (the word
for the resource in that line). The agent export file does not carry the
connections, since a binding ID belongs to one server.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node playwright protoc swag
2026-11-03 22:02:30 +00:00
Ettore Di Giacinto 8cf6153ea6 feat(connectors): let a credentials-only kind have bindings without an agent
A connector kind can declare agentOptional. A binding of such a kind may
have no agent: it holds the token, the resource list and the write mode
for the tools of a chat, and never receives or answers a message. The
GitHub kind declares it, so a person no longer makes a throwaway agent
only to store a token.

Without an agent the host refuses every setting that needs one:
listening, accepting senders, an allowlist, a notification chat,
talk-back channels, permissions, pairing and trigger approval. The
places that read the agent of a binding handle the empty value:
inbound routing, the polling loop, notifications, pairing, the capability
check (deny by default) and the grant switch. A kind that does not
declare the key still needs an agent on every binding.

The binding page makes the agent field optional for such a kind with
help text, and the list shows "Credentials only".

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node protoc swag
2026-11-03 21:29:29 +00:00
Ettore Di Giacinto d8bd3879f2 feat(studio): let a trigger proposal carry its acts, shown before the yes
A trigger run with nobody present only reads unless its trigger holds
act.* grants, and until now only the REST API could set them. An agent
that proposes a trigger can now ask for acts too:

- propose_trigger takes an optional acts list. The names must be
  act.* grants the agent of the chat holds; anything else is refused
  and nothing is stored. A read_only proposal cannot carry acts.
- The proposal stores the list. Confirming it, in Studio, by a plain
  yes or by confirm CODE, grants exactly that list. No tool changes
  the acts of a trigger afterwards. Two proposals that differ only in
  their acts are kept apart.
- The text of a proposal in a messaging app now has a Permissions
  line, in plain words with sensitive classes marked: read only, or
  what a run may do. The confirmation repeats it. A proposal that asks
  for acts is never set up without a yes, whatever the approval mode.
- The trigger REST object gains acts_held, read_only and
  read_only_why, computed with the same rules as the run, so a client
  can show why a run only reads.

Assisted-by: Claude:claude-sonnet-5-5 go ginkgo
2026-11-03 20:53:02 +00:00
Ettore Di Giacinto 3bd93e18e7 fix(chats): let the draft add-ons list answer for the draft's switches
A new chat with the sandbox switched off in the composer showed no
"switched off" notice. GET /api/chats/draft/addons took only the model
and the agent, so it always reported the default commands state.

Accept the draft's switches as comma-separated query parameters
(addons_off, builtins_off, builtins_on), check them as the create request
does, and apply them to the draft record before the commands state is
worked out. The composer sends its choices each time an add-on or a
built-in group is switched, so the notice follows the switch.

Assisted-by: Claude:claude-sonnet-5-5 go-test playwright
2026-11-03 19:38:55 +00:00
Ettore Di Giacinto 6b181cd20b feat(studio): enforce the action policy of unattended runs
A trigger run or an agent job has nobody to approve a step, and so far
the approval mode auto_readonly and the act.* grants were only recorded.
The prompt was the only thing that kept such a run from acting.

Every tool call of an unattended run now goes through one check at the
call. A tool that only reads runs. Any other tool needs the grant of its
class, and the check fails closed:

- act.files for write_file, edit_file, move_file, delete_file and the
  built-in bash;
- act.addons, or act.addons:<id>, for add-on tools whose declared kind is
  not read, plus act.spend when the add-on gates the tool with a
  permission;
- act.spend for MCP tools.

A trigger holds its new acts list within the grants of its agent. A
trigger that declared read only runs, or that a binding in auto_readonly
mode made without asking, is refused every non-read tool whatever it
holds. A binding that cannot be read counts as auto_readonly. A job
holds the act grants of its agent. Sub-agents follow their parent, and a
saved agent cannot be started from an unattended run.

A refused call answers with a plain sentence the model can pass on, not
an error, and writes one audit log line. Triggers set acts through the
REST API; a model never can. A trigger whose plan names a file it writes
keeps act.files.

Assisted-by: Claude:claude-sonnet-5-5 go ginkgo swag
2026-11-03 19:12:29 +00:00
Ettore Di Giacinto cf1041ca96 feat(ui): list agent skills on the Skills page
The Skills page asks for the skills of the saved agents with
agents=true and shows them with the agent's name. A skill of the
agent layer says who proposed it, can be removed (to the Trash) and
is not offered to other agents. The list of a plain chat or of the
agent form never carries them.

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-03 19:10:39 +00:00
Ettore Di Giacinto dcd180ced8 feat(studio): add the agent skills layer
A saved agent keeps its own skills in LocalAI/agents/<agent>/skills.
Only a chat that runs that agent sees them. They sit between the folder
and personal layers, so an agent's version of a skill wins for that
agent, and they count as granted without being listed in the agent's
skills.

An agent writes one by a skill change proposal, as before; the new
layer only reads them. A proposal for an agent skill cannot take the
name of a core skill or of a document slot (read from slots.json).
When writing-skills loads in a chat of a saved agent it starts with
"Do not write files. Call propose_change with kind skill.", and the
skills.write grant makes it visible to the agent.

The Skills list gains the agent layer, with the agent's ID and name,
and a skill can be read or removed (to the Trash) with the agent
query parameter.

Assisted-by: Claude:claude-sonnet-5-5 go swag
2026-11-03 19:10:39 +00:00
Ettore Di Giacinto c5f636d1b9 refactor(addons): move connector file bytes by handle only
A connector add-on no longer carries file bytes in the connector
envelope. The ops media and send_file need a transfer handle, and the
gateway refuses a call to them without one, or for a kind that declares
no files.

- The manifest field files.transfer, the inline file of a request and
  of an answer (ConnectorFile) and the 1 MiB cap
  (MaxConnectorFileBytes) are removed. The 2 MiB limit of a connector
  call between hosts drops to 1 MiB, because it holds text and no file.
- A kind that declares no files moves no file: the host answers a sender
  who attaches one with a plain sentence and does not offer attach_file.
  A direction left out of a declared files block still gets the host
  ceiling.
- The inline layer is gone from the file limit, from the API
  (inline_bytes) and from the Files section of the connector page.
- The Telegram and Slack add-ons keep only the streaming paths. The
  tests moved to the handle path, and the studio test rig gives every
  connector add-on a table of handles.

Assisted-by: Claude:claude-sonnet-5-5 go test node
2026-11-03 16:57:04 +00:00
Ettore Di Giacinto 2a491cb2cf refactor(addons): remove the shims for older add-on data
No release carried add-ons, so nothing reads older add-on data or
manifests. Delete the code that did.

- Connectors: the kind and the add-on of the Telegram connectors that
  core once carried are gone (LegacyAddon, KindTelegram, the stored
  binding normalization, the platform titles). A binding needs a kind,
  and its add-on is the one that serves the kind. The conversion of the
  old agent pool lists every connector as dropped instead of making a
  Telegram binding, and its notes say so.
- The library index reads only the id field of an entry, not addonId.
- A host control that has a capability block (the network policy) is
  attached by that block alone, not by a tool that replaces bash.
- The Office create tools no longer answer for the argument names of an
  earlier schema, and the identity file note about old addresses is
  gone.
- The known debt of the provider-name guard drops the entries of the
  conversion and of the resource listing shim, and its ceiling goes
  from 13 to 5.

Assisted-by: Claude:claude-sonnet-5-5 go test
2026-11-03 16:56:49 +00:00
Ettore Di Giacinto 7efb20bd1f feat(studio): read and send large connector files by handle
For a kind that declares files.transfer, the connector manager gives the
add-on a handle and not the bytes, in both directions.

Inbound, the add-on streams the file to the host. The chat upload then
reads it from the blob store, so a 6 MB PDF reaches the chat. Outbound,
the manager checks the access of the chat to the file, gives the
process that runs the add-on only the hash and the size of the content,
and the add-on pulls the chunks. The file limit of such a kind has no
inline cap: it is the smallest of the host ceiling, the ability the kind
declares and the limit the owner set for the bot, and the sentence names
the number and the layer. A kind without transfer keeps the 1 MiB inline
cap, and its sentence says so.

Checks: the files of one message may add up to three times the limit;
the media type that the add-on states must be among the types the kind
declares; an outbound file must have a type that the kind declares and
must not start with an HTML page. Every transfer writes one audit line
with direction, binding, size, result, layer and time, and no name or
content. The binding page no longer says "larger files need streaming"
for a kind that streams.

The specs run the real Telegram and Slack add-ons, the real gateway and
the real file operations over scripted platform APIs, with a 6 MB file in
each direction.

Not in this change: a daily byte counter per binding, and a virus scan
(the table has a hook that accepts everything).

Assisted-by: Claude:claude-sonnet-5-5 go test
2026-11-03 15:54:45 +00:00
Ettore Di Giacinto 15e2321d50 feat(connectors): edit the permissions of a binding on the shared connector page
Bindings saved before permissions were enforced have no capabilities, so
they deny files from senders, triggers, memory writes and talk-back, and
the owner could only fix that through the REST API.

The shared connector page now has a section for it, so every connector
add-on gets it: a switch and a description for each capability with the
sensitive ones marked, the role matrix (never a sensitive capability, never
above the ceiling), pairing with a code shown once, the approved channels
for talk-back, and a read-only view of the access check with the latest
decisions. A binding with no capability shows a notice that says why the
features are off, with an "Allow the usual" button that turns on the
non-sensitive everyday set.

The kinds endpoint returns the closed list of capabilities with a title,
a description and the sensitive and usual flags. The field is additive.

Assisted-by: Claude:claude-sonnet-5-5 go node playwright swag
2026-11-03 15:36:21 +00:00
Ettore Di Giacinto eaea737b53 feat(connectors): let the owner choose how chat triggers are approved
A connector binding gets an approval setting: ask, auto or auto_readonly,
with four limits for the two automatic modes (live triggers, minutes
between runs of a recurring trigger, runs a day, lifetime in days). The
setting is generic, so every connector add-on can show it, and the
shared connector page of the add-ons now draws it. A binding that sets
nothing asks, as before.

This change stores, validates and exposes the setting. The next change
acts on it.

Assisted-by: Claude:claude-sonnet-5-5 go swag node
2026-11-03 14:59:30 +00:00
Ettore Di Giacinto 24c837e453 feat(studio): grant built-in capabilities to a saved agent
A saved agent now has a Builtins list: the capabilities its owner gave
it (memory.write, profile.write, skills.write, pages.write,
dashboard.write, triggers.propose, triggers.run, talkback.channels,
talkback.new_channel, files.upload, act.files, act.addons, act.send,
act.spend and the opt-in group saved_agents). The list is empty by
default, and nothing turns a grant on for a template, an import or a
write that omits the field.

A chat of the agent holds the grants the user may use at the turn
(agent scope). A tool exists when the grant is held, the chat did not
switch its group off and the run can hold rights: a sub-agent or a run
that started by itself never does. propose_change and revert_change
now exist only for a chat that holds the grant of at least one kind,
and the schema lists only those kinds.

propose_change gains the kinds skill, page and dashboard. A skill is a
SKILL.md for the personal or the agent layer, and a skill with
locked: true is refused, also as a shadow of a locked personal skill.
A page is a set of files checked against the Pages rules and written
as one change set. The dashboard is a home.json checked against its
schema. All three are re-checked when the owner confirms or edits.

GET /api/chats/agents/builtin-grants lists each grant with its title,
description and sensitive flag. The agent record, the write body and
the export file carry builtins; an old file imports as before and an
unknown name is dropped in an import and refused in a write.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt swag
2026-11-03 14:56:42 +00:00
Ettore Di Giacinto e790271c63 feat(addons): let the GitHub add-on write, under a per-connection write mode
A connection now has a write mode, chosen on the add-on page: off (the
default), propose (each change waits for the person to confirm) or auto
(a chat of the person makes it at once). The host enforces it for every
write tool of an add-on that holds a credential, before the add-on is
called, and holds the call to the repository list of the connection.

Host:
- Binding.WriteMode, stored in both stores and in the connector API
  (write_mode). Absent means off, so existing bindings stay read only.
- A credentialed add-on may declare write tools. They name the resource
  with the credential slot argument, never resourceId. A tool can set
  approval "always", a ceiling that keeps a proposal even in auto.
- The proposal broker fetches the credential for the preview and again
  for the apply, so it is never stored. A mode lowered between the
  proposal and the confirmation refuses the apply. A proposal that
  awaits a person cannot be confirmed by a chat turn.
- New capability name tools.write for the roles a connector kind
  declares.

Add-on, tools (kind write): comment-issue, comment-pr, create-issue,
edit-issue (title, close or reopen), add-labels, review-comment-pr.
They preview plain facts, refuse a stale target, cap and screen the
text for secrets, and keep a receipt journal plus a hidden marker in the
text, so a change whose answer was lost is not sent twice.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo node
2026-11-03 14:53:20 +00:00
Ettore Di Giacinto b8626132ae feat(connectors): let the agent write to listed channels with send_message
A connector chat whose sender holds talkback.channels gets the tool
send_message. It writes in the conversation, or as a new top-level
message in a channel that the owner listed on the binding (id, label,
approved; at most 10). The tool names no platform.

An unlisted channel is refused and the error lists the ones that are.
A listed channel that the owner did not approve needs the sensitive
talkback.new_channel for each message, so only a paired sender can use
it. The agent cannot add or approve a channel, so text it read in a page
or a file cannot widen where it writes.

Messages go through the outbox (rate limit and queue), with the size cap
and the mention breaking. Each attempt adds an audit row with channel,
kind, capability, length and a hash, never the text.

A trigger can report to an approved channel with delivery target
channel. The binding, the capability and the listing are checked again
when the run ends; any failure sends the notice to the Inbox.
LOCALAI_DISABLE_CONNECTOR_TALKBACK turns both off.

Assisted-by: Claude:claude-sonnet-5-5 go swag
2026-11-03 14:42:21 +00:00
Ettore Di Giacinto b15905e90f fix: restore the files that the previous commit removed by mistake
The previous commit was made on a stale base and so reverted work that
landed on master before it. This commit puts every file back to the
state of its parent's parent. The permission enforcement of the
previous commit stays.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 14:33:05 +00:00
Ettore Di Giacinto 9e6d6520bd feat(connectors): enforce add-on permissions on each connector turn
The capabilities, role grants and pairing that a connector binding
stores now decide what a sender can make the agent do.

Authorize is one pure function: the capability must be in the owner's
ceiling for the binding and the sender must be on the allowlist. A
sensitive capability then needs a paired sender; roles never count for
it. Any other capability needs a pairing or a role that the owner
granted and that the kind lists. The default is no.

Each message gets a grant for its sender. A message that joins a
running turn narrows it to what both senders may do. The tool builder
offers a gated tool only for a held capability, and the tool checks
again when the model calls it, so a call that gets through is refused
with the reason. Today this covers propose_change (memory and profile
kinds), revert_change, the trigger tools, and files sent to the bot. A
chat worker gets the grant with the start request.

The capability gate that the trigger features ask is now answered by
Authorize, and the application sets it. Other features can add a
global switch with AddCapabilitySwitch.

Every use records an allow or deny row (time, binding, sender IDs,
roles, capability, result; no text or codes). GET
/api/chats/connectors/{id}/access returns the effective permissions
per kind of sender and the latest rows, for an add-on settings page.

A binding saved before this change has no capabilities, so its senders
lose these features until the owner enables them.

Assisted-by: Claude:claude-sonnet-5-5 go swag
2026-11-03 14:32:01 +00:00
Ettore Di Giacinto 15740acbb9 feat(studio): describe every built-in tool switch and let a saved agent set them
The Tools menu of a chat listed a ninth built-in group, "changes", by its
raw ID with no text, and only some rows said what a switch did. The
registry now carries a title, a description of what the model can do when
the group is on, and what happens when it is off, for every group. Groups
that nobody described fall back to the label and description of their
first tool, so a new tool group needs no change in the interface.

An add-on row can show the title and description its manifest declares:
the broker may implement the optional AddonLister, and the lists carry
both fields next to the ID.

A saved agent gets builtins_off and builtins_on. builtins_off is a
ceiling: a chat of the agent shows the group off with a reason and cannot
switch it on, and a sub-agent follows it. builtins_on lists the opt-in
groups the agent's chats start with. Both are additive columns, a write
that does not name them keeps the stored ones, and groups the server does
not offer are dropped so an imported file still loads. An agent with
switches exports as version 3, so an older server refuses the file
instead of giving the agent every tool; a single-folder agent without
switches still exports as version 2.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt swag
2026-11-03 14:30:07 +00:00
Ettore Di Giacinto e0da582988 feat(addons): credential slot, resource allowlist and rate-limit error
Add a manifest credentials declaration, host-enforced per-binding
resource lists, Request.Credential and ResourceKey set by the host, and
a typed ErrRateLimited that reaches the model as rate_limited.

Assisted-by: Claude:claude-sonnet-5-5 [go]
2026-11-03 14:20:12 +00:00
Ettore Di Giacinto db596e1696 feat(studio): accept a list of agent folders in the API and the agent file
POST and PUT on /api/chats/agents take folders, a list of path and mode
rows, up to eight. The old folder field still works and is read as a
list with one entry. Responses carry folders, the effective mode of each
row and a notice with the count of shared folders a chat does not mount.

A folder inside another folder of the same agent takes the mode of the
outer one, because the file store gives a node the highest mode among
the roots above it and cannot hold a read only folder inside a write
folder. The editor data shows that mode.

An agent with one folder still exports as version 2. An agent with
several exports as version 3 with folders. A server that reads version 2
refuses version 3. Import keeps a folder that is not found as a row
that is not chosen.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 14:02:02 +00:00
Ettore Di Giacinto ab78cd0301 feat(studio): propose triggers from a connector chat and report back there
A connector chat can now propose a trigger, the person answers it in the
chat, and the run reports to the chat.

The trigger tools propose_trigger and list_triggers are offered in a
connector chat when a capability gate allows triggers.propose for the
binding. The gate is a small interface on the service with a deny-all
default, so nothing changes until the application sets one. Change and
delete stay in Studio.

A proposal from a chat stores the binding, chat and thread, a four
character code and a 24 hour expiry. The host writes the proposal and the
code in that chat. "confirm CODE" or "cancel CODE", as a whole message in
the same chat and thread, is handled before any model turn and needs
proposals.confirm. A binding may make 3 proposals an hour. Proposals that
lapse are marked expired, and the proposal card shows that state.

A trigger carries a delivery: the Inbox (default) or the chat of the
proposal, with the modes always, on_change and on_problem. The host writes
the last message of the run, cut and with broadcast mentions broken; a
message of exactly [SILENT] writes nothing; three failures in a row raise
one alert per 6 hours. A failed delivery falls back to the Inbox. Host
messages of a binding go through an outbox of 20 a minute that queues the
rest. While triggers.run is off, runs of such a trigger are skipped, not
replayed, and the record is unchanged. The audit records each step with a
hash and no text.

Assisted-by: Claude:claude-sonnet-5-5 go test ginkgo eslint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 14:00:31 +00:00
Ettore Di Giacinto 1f481b0747 feat(connectors): add-on roles and permissions, and sender pairing
A connector kind can now declare the roles it gives to a sender and the
capabilities it can grant to a role. The owner of a binding enables a
ceiling of capabilities and maps roles to them. The host stores these
values and returns them with the binding and the kind list, so an add-on
settings page can show them.

Capability names are a closed list owned by the host. Sensitive names
may be listed by a kind but are never granted to a role; only a paired
sender can use them.

Pairing proves that the owner controls a platform account. The owner
asks for a one-use code (valid ten minutes) and sends it as a direct
message to the bot from an allowlisted account. The host checks the code
before any model turn and never logs it. A binding holds three paired
senders, a pairing ends after 90 days, and five wrong codes in an hour
lock the sender out for an hour.

Contract: connector kinds gain optional roles and permissions, and a
message gains optional roles. They need manifest api 2, which the host
already accepts; protocol is unchanged and manifests that declare
nothing new behave as before. The host drops roles a kind does not
declare.

Enforcement of the capabilities by feature is not part of this change.

Assisted-by: Claude:claude-sonnet-5-5 go swag
2026-11-03 13:52:56 +00:00
Ettore Di Giacinto e0173d8096 docs(api): drop thread_context from the connector create description
Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:57:44 +00:00
Ettore Di Giacinto c8cb68b434 refactor(studio): always read a thread when the bot is mentioned in it
The thread_context binding setting is removed. When the kind declares
the history ability and the bot is mentioned in a thread, the host
reads the thread once for a new chat, as before. The other guards stay:
the ability check, the bound, the untrusted block, and a failure never
blocks the answer.

A binding stored by an earlier build still loads. The JSON store
ignores the unknown field. The GORM row no longer maps the
thread_context_off column; AutoMigrate does not drop it and it accepts
NULL, so reads and writes keep working. No data is migrated or removed.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:54:33 +00:00
Ettore Di Giacinto 11d1f972ed feat(chats): report the host ceiling with the file limits
The Settings page states the ceiling in effect from this field.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:47:51 +00:00
Ettore Di Giacinto 86a93259ce feat(chats): show and set the file limits of a connector binding
The connector list shows, for each binding and each kind, the limit in
effect in both directions, the layer that sets it, the ability the kind
declares and the most an owner may set. A binding accepts
file_receive_max_bytes and file_send_max_bytes; a value above the limit
of the kind is a 400.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:47:50 +00:00
Ettore Di Giacinto d676d629a0 feat(studio): filter the chat list by origin
GET /api/chats takes an optional origin parameter: person (chats a
person started), automated (every other chat) or one origin kind such as
trigger or connector. The limit counts the chats kept, so a flood of
chats that a connector or a trigger started cannot push the chats of a
person off a page, and the other way round. Without the parameter the
list is unchanged.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:41:13 +00:00
Ettore Di Giacinto 0e50c3f9ca feat(addons): let a connector kind declare per-binding options
A kind can declare options with a few values and a default (manifest
api 2). The host checks the owner's choice against the declaration,
stores it on the binding, shows it in the REST API and sends the value in
effect with every poll. The add-on decides what the option means.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:30:11 +00:00
Ettore Di Giacinto f20b7250ad feat(studio): add the thread_context switch to connector bindings
A binding reads the earlier messages of a thread by default. The new
thread_context option turns that off, and the kind list says which kinds
can give a thread's messages. It is stored like send_files: the zero
value keeps the default.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 11:30:11 +00:00
Ettore Di Giacinto fc6abfca8c refactor(studio): name the network from the add-on's declaration
The chat's network view, the administrator view and the line the model
reads about the network of its commands now take their name from the
capabilities.network an add-on declares, or from the title of its
commands tool when it declares none. The sentences no longer say
"Sandbox" or "the sandbox". The API gains a network object with the
title and the preset titles next to the existing addon field.

The text of the built-in bash and of the host-exec setting names an
add-on that provides bash instead of one add-on.

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