Commit Graph
1090 Commits
Author SHA1 Message Date
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 cde3a2d6d9 feat(ui): show search hits with highlights, filters and a link to the message
The chat search of the Chats page and of the Ctrl or Cmd+K dialog kept
the list of the newest chats and only filtered it, so a chat that was not
in the list could never be found. From two characters the list now shows
the answer of GET /api/chats/search: the hits best first, each with the
chat, who wrote the line, its date and a snippet. The matched words are
marked from the offsets the server sends and drawn as text nodes, so
message text is never read as HTML. Filters for agent, source and date
narrow the search. Arrow keys move through the hits, Enter opens one, and
Escape clears the search before it closes the dialog. The list shows a
searching, empty, failed (with Try again), still-indexing and partial
state.

A hit opens its chat at the message: /app/chats/<id>?seq=<n> scrolls to
the line when the transcript has it and marks it for a moment, again on a
second click. The new strings are in every locale.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:47:19 +00:00
Ettore Di Giacinto 21bec51bad feat(ui): show add-on titles and descriptions in the language of the user
The library cards, the first-run list, the install dialog and the
upgrade dialogs now name an add-on by its translated title and
description, taken from the library entry or the installed record.
A language without text, and English, keep the English text, field by
field. The installed list and the library page are in the alphabetical
order of the language of the user, and a search finds an add-on by its
translated name and by its English name.

The Home commands test and the provider strings test read the English
title now that the title of an add-on is a language map.

Assisted-by: Claude:claude-sonnet-5-5 javascript
2026-11-04 17:46:53 +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 26a5e46c2b fix(ui): keep the add-ons list on screen while the server is busy
A page loaded while the owner was busy had no list to draw, and a failed
read could look like an empty install. The apps list read now carries
the stale mark of the server. The page keeps the last list (from the
server, or from the copy this tab stored), shows a notice that names the
add-ons being upgraded, offers a retry, and asks again with the existing
backoff. Each add-on that is changing shows "Upgrading..." with its
phase. An error, or a stale copy of an empty list, is a load error, and
the empty state appears only when the owner answered with an empty list.

The new strings are in all eight locales.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:41:18 +00:00
Ettore Di Giacinto 8011e54ddd fix(addons): answer reads while the owner runs an upgrade
An install or upgrade holds the owner's state lock through the backup,
the commit and the start of the new generation. The apps list, the host
access overview and the installed list took the same lock, so they
waited for the whole operation and the frontends timed out after 65 s.
The Add-ons page then had no list to show.

Reads now work on a snapshot of the journal that every journal write
publishes, and never wait for an operation. The apps list reports the
running operation of each add-on (kind, phase, since), and the owner
records the stage of an upgrade through the start of the candidate. A
second upgrade of an add-on that is changing is refused with a conflict.

A frontend waits eight seconds for a quick read of the owner. When the
owner does not answer, the apps list returns the last list it got for
the same person, marked stale for ten minutes. Without such a list the
route answers a typed owner_busy or owner_unavailable 503, and the pins
route no longer drops pins it could not check.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:41:18 +00:00
Ettore Di Giacinto 783bea59f8 fix(ui): stop long unbroken names from widening the page
A name with no spaces (an agent name, a path, a URL, an id) pushed the
document wider than a phone and made the page scroll sideways.

- page header: on a phone the header is a wrapping column, which sizes
  its one line to the widest item. Make it a non-wrapping column and let
  the title and supporting line break anywhere.
- agent form: the back button, the collection and name chips, the agent
  page facts and the connection group heading now wrap long text.
- add e2e/no-horizontal-overflow.spec.js: it draws the main pages with
  unbroken long content at 390 and 768 px, light and dark, and asserts
  that the document is not wider than the window.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:36:23 +00:00
Ettore Di Giacinto e48bf37995 feat(addons): translate the add-on pages into every language of the web UI
The shared connector page kept its words in English text maps inside the
components, and only Studio shipped translations. Move the maps to JSON
bundles (addons/internal/connector-ui/src/i18n) and add bundles for the
Telegram, Slack, Discord, GitHub, Jellyfin, Home Assistant and Talk pages
in German, Spanish, French, Indonesian, Italian, Korean, Brazilian
Portuguese and Simplified Chinese. English stays the default, and a key
that a bundle lacks reads in English.

Plural sentences now go through the translator, so each language picks
its own plural form. The "More" and "optional" labels, the sender line and
the dates follow the language of the page instead of the browser.

A new gate, addons/internal/i18ncheck, fails when a language lacks a
bundle, a bundle lacks a key or has an extra one, a placeholder differs
from English or a value is empty. It runs in make test-i18n and so in the
UI unit tests of CI, with no baseline. Playwright specs render the pages
in Italian, check that no language runs text out of its box on a phone,
and save screenshots when UI_SHOTS_DIR is set.

Assisted-by: Claude:claude-sonnet-5-5 node
2026-11-04 17:30:57 +00:00
Ettore Di Giacinto 48d6d03f3d fix(studio): write the connector refusal texts as whole sentences
The texts for a blocked capability put a noun phrase into slots that
need a verb, so some read badly ("was not given writing to other
channels", "before reminders and scheduled tasks"). Each capability now
has an action phrase in the infinitive and a noun phrase. Every code is
a complete sentence built from the action phrase, for the owner and for
the sender and the model, and most say what to change.

The note for the model now sets whole sentences apart with a space and
asks the model to relay the reason plainly. A table-driven test renders
every code for every capability and checks grammar, length and that no
internal name leaks. Codes, JSON shapes and behaviour do not change.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:18:41 +00:00
Ettore Di Giacinto 78044d4def feat(ui): call the collections page Knowledge, not Memory
The word "Memory" named collections of documents, notes about the
person, and RAM in the same navigation. Collections are now Knowledge:
the sidebar item, the page title and text, the Home action, the agent
form section and the chat tool group. The tool group "Memory and
profile changes" is now "Suggested changes", since it is the generic
ask-first mechanism for notes, rules, skills, pages and the dashboard.
The Settings section "Memory and models" is now "Models in memory".

Only display strings change. Ids, routes, locale keys, group ids,
grants and stored data stay as they are. All nine locales are updated;
the docs name the Knowledge page and say which search the default
engine does.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:17:38 +00:00
Ettore Di Giacinto 59d9c7556e fix(connectors): do not list the chosen sender as unchecked
The panel "What this bot can do" showed "Not checked yet: who is
sending" although the sender is the one picked in the selector. It now
lists only the links that were really not checked, and hides the line
when none remain. Capability titles are shown as sentences with the
product name spelled LocalAI. The test host messages now use the words
of the real host, so the specs and the screenshots read as in use.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 16:07:23 +00:00
Ettore Di Giacinto 4298715291 feat(connectors): show what a connection will do, and its readiness
The connection page of a messaging add-on listed what a sender could do
for the saved settings only, low in the form. Two blocks now sit on the
page and read the form as it is.

Readiness, at the top of the form, has one row for the token, agent, who
may use the bot, permissions, triggers, files and notifications. Each row
has a state, one sentence and at most one fix. Only rows that need
attention show at first. A ticked permission that nobody can reach, such
as a list of channels with no person, is named.

"What this bot can do" replaces the old list. For a chosen kind of sender
it shows each permission as allowed or blocked, the sentence of the host
for the first blocker, and one button taken from the kind of fix: jump to
the setting, open the agent in a new tab, open Settings (an administrator
only), pair a sender or add a person. It asks the access preview 400 ms
after the last change, drops stale answers, works for a connection that
is not saved yet, and says when the form has unsaved changes. The latest
decisions stay below it.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 16:07:23 +00:00
Ettore Di Giacinto 6be3a96083 feat(addons): add the Home Assistant add-on
Chats and agents can read a Home Assistant instance and, if the person
allows it, switch things in it.

Read tools: server summary, areas, entity search, state, history,
logbook and the catalogue of callable services. Results are bounded,
marked as untrusted data, and stripped of pictures, access tokens,
secrets and locations.

Write tools: call-service for quiet services (lights, switches, input
booleans, media transport) follows the write mode of the connection;
call-physical-service always needs a confirmation. A deny-by-default
allowlist decides which services a model may call. A hard exclusion
list that no setting can lift covers locks, alarms, sirens, valves,
cameras, garage-type covers, restarts, reloads and shell, system and
add-on management. Calls name entities one by one, up to a cap.
Entities can be excluded by domain, area, pattern or label.

One connection is one instance: the token is the credential and the
address is its one allowed resource, so the host refuses any other
address before the add-on runs. The add-on speaks only to that address,
refuses redirects, verifies TLS, and accepts a self-signed certificate
only through a pinned SHA-256 fingerprint.

The policy lives in the add-on data folder and is edited on the add-on
page by an administrator. For that, the host no longer asks a
credential for a uiOnly tool of a credentialed add-on, since the page
has no chat to name a connection.

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-04 15:08:10 +00:00
Ettore Di Giacinto 36c405b321 feat(addons): add the Discord connector add-on
People talk to a saved agent from Discord, in direct messages and where the
bot is mentioned in a channel or thread. The add-on keeps one Gateway link
per bot token (discordgo runs hello, identify, heartbeats and resume), queues
the messages that address the bot, and hands them to the host's poll. It
sends, edits, splits at 2000 characters, moves files by handle (10 MB), and
reads thread history.

Safe defaults: guild channels answer only on a mention or reply, or in a
thread the bot follows; bots, webhooks and system messages are never read;
every message carries allowed_mentions with an empty parse list and mentions
in model text are broken up; a thread's chat is its parent channel so an
allowlist of channels covers it. No typing indicator, no reactions, no
embeds, no slash commands.

Tests use a fake Gateway (hello, identify, ready, resume, heartbeat ack,
message and thread events, reconnect, close codes) and a fake REST server
(429 with Retry-After, create, edit, upload, history, CDN). The page has its
own node tests and a browser spec.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 15:01:02 +00:00
Ettore Di Giacinto f4f36ab746 feat(studio): confirm change proposals in the chat of a messaging app
A note, a profile, a skill, a page or a dashboard change that the agent
proposed in a messaging chat could not be confirmed there: a plain "yes"
went to the model, and the tool result sent the person to Studio, which
the connector prompt says they do not read.

The host now writes the question in the chat, with the kind, the scope
and the lines that will be saved, and answers the reply before any model
turn. yes/ok/confirm saves, no/cancel drops, a four-character code picks
one when several wait, and "undo" takes back a save for 30 minutes. Only
a sender with proposals.confirm may answer; the kinds that change what
the agent may do need a paired sender, and in a shared chat only the
sender who asked. The tool result tells the model what the host did, and
the Studio wording stays for Studio chats.

The permission text of "Confirm changes from the chat" now says what it
covers. The proposal source keeps the asking sender.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 14:54:15 +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 1ae80d3a15 feat(connectors): add /new to start a fresh chat from the app
A connection kept one chat per person for good, with no way to ask for
a new one. A person can now send /new (or !new, for Slack, which keeps
messages that begin with a slash for itself) as the whole message.

The host reads the command after the pairing check and before any
model turn, so the agent never sees it and the logs record only the
ids. It drops the link from the person, or from the shared thread or
group, to its chat. The old chat stays in Studio. The link is in the
shared store, so it works with several frontends and for every
connector add-on.

In the chat mode and in a thread of a group, everyone shares the chat,
so only a paired sender may reset it. The thread mode answers that
every message already starts a new chat. A chat with a running turn is
left alone and the sender is asked to repeat the command later.

Telegram did not treat /new@BotName as addressed to the bot in a
group; it does now, and keeps the text. The connection page gets one
help line under Conversations.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 13:35:18 +00:00
Ettore Di Giacinto af570ff17d feat(connectors): add Runs may act to the approval settings
A trigger proposed from a chat can carry act.* permissions only when
the connection lists them in approval.acts. The connector settings
page had no control for it, so runs always read only.

Add a switch "Runs may act" under the approval mode of Propose
triggers. When on, four sensitive checkboxes (files, add-ons, send,
spend) say what a run may do. A box is disabled with a reason when the
agent lacks the matching grant, but a ticked one stays ticked and can
be unticked. A line says how the approval modes treat acts, and the
row of Propose triggers lists the acts that are set.

The page sends acts together with the mode and limits, because the
server replaces the whole approval object. An empty list means runs
only read. Names outside the four are rejected before saving.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 13:19:09 +00:00
Ettore Di Giacinto 388be1fa1d ci: free runner disk for add-on jobs and fix the Memory delete spec
The add-on jobs stop in prepare.sh when less than 20 GiB is free, and
the hosted runner image no longer leaves that much. Add a small script
that removes the unused preinstalled toolchains in parallel and prints
df before and after, and run it first in both jobs.

The Memory delete spec expected one status element. The page live
region and the toast container both carry role=status, so two match.
Check the page live region on its own and the toast as the second.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:33:22 +00:00
Ettore Di Giacinto eb605031e6 test(ui): mount the settings harness inside a router
The built-in tools section reads the location to scroll to a row, so a
render without a router threw.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:22:31 +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 3e84fe671f Add one notice for off or unavailable states, and use it in chat and add-ons
Notice draws what is off, why, who can fix it and one fix button, in
that order, with a live role set by severity. FixNotice draws the same
markup for add-on UIs, styled by the host's ui.css.

The chat notice for commands now says that the server's built-in bash
is off. An administrator gets the Settings row and a link to the
Add-ons page; anyone else is told to ask an administrator. Hide keeps
working. The Add-ons library card, the update notice and a turn on
refused as installed_incompatible use the notice, with an Upgrade fix.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:19:45 +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 bb3933e9d5 feat(connectors): put the trigger approval under Propose triggers
The approval mode of proposed triggers sat at the bottom of the
connection page, after roles, channels and pairing, and was easy to
miss. Show it as an option of the permission Propose triggers: nested
and indented, greyed with a reason while the permission is off, with
its limits in a closed Limits list. The row says the current choice
("Proposals: asks first") without opening anything. The bottom section
is gone; an older host with no capability list still shows it as one
block. The saved approval object does not change.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:17:25 +00:00
Ettore Di Giacinto b349946b49 feat(connectors): state the permission rule and narrow the pairing notice
An allowlisted person can use the ticked permissions and only the
sensitive ones also need a pairing. Say that rule under the title of the
permissions form, and show the pairing notice only when a sensitive
permission is ticked while nobody holds a live pairing. Add a notice for
an allowlist that holds only channels, whose members are not trusted one
by one.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:17:25 +00:00
Ettore Di Giacinto 22ca7403f1 feat(ui): one switch per capability in the agent form
The agent form switched the same capability in two places: a Tools
switch and a permission at the bottom that changed what the switch
meant. Show one switch per capability and put its permissions under it
as options, greyed with a reason while the switch is off. The stored
data and the API do not change.

Triggers, memory and profile changes, files and saved agents each hold
their options. Saved agents is one switch: either stored value turns it
on, off clears both, and on adds only the group so it never turns off
the defaults of the messaging connections. Reply, start new channels
and the permissions that apply when nobody is watching have a section
each. Every switch and option has one line that says what it lets the
model do, and the three trigger switches have names that cannot be
mixed up. The Permissions fold is gone: its list is now in Tools.

The form names a messaging connection of the agent that does not allow
an option the agent holds, with a link to that connection. The chat
Tools menu draws the same rows and shows the held options read only.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:17:25 +00:00
Ettore Di Giacinto 9a98a39748 feat(connectors): copy or fill permissions from the agent
The permissions form of a connection gets two buttons that fill it from
the agent of the binding. "Allow the usual set" ticks the usual
capabilities the agent holds and says which ones it left out because
the agent does not hold them. "Same as the agent" copies, once, every
capability the agent holds that has a binding capability, after a
confirmation that lists the sensitive ones. Both only fill the form, and
are off with a reason when the binding has no agent or its permissions
cannot be read. A capability the agent does not hold cannot be switched
on.

The form also says when ticked capabilities cannot be used yet: the host
lets a sender use one only with a pairing or a role that holds it, so
with nobody paired and no role grant the ticked ones do nothing. The
notice links to pairing. A link with ?binding=<id> opens that binding at
its permissions.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 10:17:25 +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 8b7163f301 Show the update status on an installed add-on's library card
The library card of an installed development build showed only the
installed version when the check had no answer, so a missing or failed
check looked like nothing to update. The card now says when the status
is unknown, when the check failed and when the add-on is up to date.

Add checker coverage for an install that recorded the channel name and
for a registry lookup that fails: neither reads as up to date.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 07:27:40 +00:00
Ettore Di GiacintoandClaude Sonnet 5.5 c3566febd3 Report every add-on update in one check and say why an old package will not start
Checking several add-ons of one library in a row reached the registry
only for the first, because the library interval also throttled the
per-add-on tag resolution. The others answered unknown at once, so a
check-all reported one update and the next pass the next one.

The library interval now spares only the library fetch; each add-on is
throttled by its own interval.

Turning on an installed package whose manifest this host refuses now
fails with the typed installed_incompatible cause and a message that
says to upgrade, instead of the generic owner-unavailable 503. The
upgrade of such a package stays allowed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 07:26:08 +00:00
Ettore Di Giacinto 48bc71c38a test(ui): compare the composer height with a sub-pixel tolerance
Chromium reported 175.99998 px before focus and 176 px after, so the
exact toBe check failed in about half of the runs. Allow half a pixel.

Assisted-by: Claude:claude-sonnet-5-5 playwright
2026-11-03 23:14:30 +00:00
Ettore Di Giacinto ca284491aa test(addons): expect the Host access tab last in the keyboard spec
The host access step added a fourth tab after Sources, so End now
selects Host access. The spec still expected Sources and failed on
every run.

Assisted-by: Claude:claude-sonnet-5-5 playwright
2026-11-03 23:14:30 +00:00
Ettore Di Giacinto 653ba1afb8 fix(addons): stage no file while the UI host grants are locked
Sign-in off, or a policy file that is the only source, locks the UI host
grants. An unsigned file must not be staged or installed then: the owner
refuses file-begin with locked_by_deployment and the install dialog shows
the lock and disables the install.

Assisted-by: Claude:claude-sonnet-5-5 go eslint
2026-11-03 22:53:34 +00:00
Ettore Di Giacinto 398c1d56cb test(addons): list the file routes in the registered route inventory
Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto cbe84cf062 feat(ui): install an add-on from a file
Add an Install from file button to the Add-ons page. The file goes to the
owner, then the install dialog opens with the host access step, the file
sha256 and the notice that nothing signs a local file. An upgrade does
not turn the add-on on, an older version needs a tick, and the same
version is refused. The link that local-ai addons install prints opens
the dialog for the staged file. The list, the details and the host access
page show that the add-on came from an unsigned local file.

Assisted-by: Claude:claude-sonnet-5-5 eslint playwright
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto d71df56a66 fix(addons): take a local file only with the dialog's approval
The plain typed install and the owner's automatic upgrade never install
a package from a file. Only the host access install, which needs the
administrator's own browser session, sets an approval, so a key or a
tool cannot install a staged file. Add auth tests for the file routes.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto cbec432ed6 feat(addons): upload a zip to the owner and install it from the Add-ons API
POST /api/addons/files streams the zip to the owner in chunks over the
host access call, so a frontend of a split deployment keeps no copy and
the owner enforces the size limit while it receives. GET and DELETE read
and remove a staged file. The install is the existing host access
preview and install with sourceId local-file, so it keeps the human
surface rule, the nonce and the digest binding. The install body gains a
downgrade confirmation.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto a2e83e7329 feat(ui): show locked host grants in the install dialog and the Host access page
When the owner keeps UI grants locked because sign-in is off, the
overview and the preview say so. The Host access page and the install and
upgrade dialogs show the operations read-only with one sentence that
says why, and the dialogs approve no operation. The sentence is added to
every locale.

Assisted-by: Claude:claude-sonnet-5-5 node
2026-11-03 22:52:09 +00:00
Ettore Di Giacinto fd634c5f5c fix(addons): fail closed on audit errors at install, audit refused callers and restarts
Staging the grants of an install now goes through Store.UpdateAudited, so
an install fails and the document stays as it was when the audit line
cannot be written.

A write from an API key, an MCP session or a tool call was refused in the
HTTP layer before it reached the owner, so the refusal left no audit line.
The route now hands the refused write to the owner without the human
mark; the owner refuses it and records the credential kind, never the
credential.

Manager.HostGrantsChangedReport names the add-ons the re-sync started or
stopped, and the owner writes a restart-for-grant line for each. The
event constant was never used before.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:09 +00:00
Ettore Di Giacinto 23beb6dc80 feat(chats): let a person choose how memory notes are saved
A memory note is still a proposal that the person confirms by default.
An add-on can declare the user setting with the role memory.save-mode,
and the person then picks: ask first, save it and tell me, or never.

With auto, a note from the person's own Studio chat is applied at once
through the whole proposal path (grant, gate, checks, rate limit, audit)
and confirmed as the person, so it is one change set that revert_change
and the Undo on the chat restore. The chat shows "Saved to memory" with
Undo and no confirmation card. The record carries auto: true and the
audit says the write was automatic.

Safety rules live in the host. A connector chat always asks, a trigger or
job has no tool, a saved agent cannot widen the choice, and a failure to
read the setting asks. At most 5 notes are saved unasked in a turn; a note
over 4 KiB or one that looks like it holds a secret becomes a normal
proposal. With never the memory kind is hidden from propose_change and a
stray call is refused. Only memory reads the setting.

Assisted-by: Claude:claude-sonnet-5-5 go node playwright
2026-11-03 22:46:22 +00:00
Ettore Di Giacinto 7ce4507da1 feat(addons): keep per-person settings that an add-on declares
An add-on can declare userSettings in its manifest: a key, the allowed
values and a default. The host keeps each person's answer in the account
database every frontend shares (one file when accounts are off), checks
every write against the declaration, and never asks the add-on.

GET and PUT /api/addons/{id}/user-settings read and write them. A write
needs the person's own browser session, so no API key, model or tool can
change one. A setting may carry a role that the host reads for its own
rules; the host fixes the values and the default of a role so a manifest
cannot loosen it. Pages reach their settings through sdk.userSettings,
which is bound to the add-on that calls it.

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-03 22:46:22 +00:00
Ettore Di Giacinto eaf5401e11 fix(ui): draw one focus indicator on the composer
The message box kept the global 2px focus-visible outline: that rule
ties with .home-textarea on specificity and loads later, so the box
showed its own ring inside the card ring, a double border.

The message box now draws no outline or shadow on focus. The card keeps
the one indicator, a 1px inset accent edge plus a faint 2px ring (was
3px), with no transition under prefers-reduced-motion. The chips and
buttons keep their own keyboard rings. Home and chat share these rules.

Assisted-by: Claude:claude-sonnet-5-5 playwright eslint
2026-11-03 22:42:47 +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 271e7cad67 fix(ui): draw proposal cards after the final text of a turn
A turn can hold several model messages and tool runs. The cards of its
proposals (memory and profile changes, triggers) were drawn under the
message that followed the tool call, so a turn like "propose, say a
first line, read a file, explain" showed the card above the explanation.
While a turn streamed, a card read mid-turn sat above the streaming
text as well.

placeProposals now moves the cards of a turn to the end of it: onto the
last model message, or, while the turn streams or has no message yet,
into a row that the page draws after the streaming text. The tool lines
and results keep their place in the fold. Cards that were answered
already draw as compact lines, so they need no other rule.

Assisted-by: Claude:claude-sonnet-5-5 playwright eslint
2026-11-03 22:30:01 +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 270a3a12ea feat(addons): match the host access routes to the React client
The confirm header names what the administrator acted on: addonId@digest
for an install, the add-on ID for an edit or a revoke, "blanket" and "deny"
for the two switches. The operation ID is optional on every write but the
install; the receiving host makes one when the caller sent none.

Preview and page statuses are allowed, not_allowed, blocked or unsupported
in both. A preview names what an upgrade adds, using the identity of the
installed package. The page carries the library, the new optional access and
the required operations that keep an add-on from running. "keep" relabels
named blanket grants as the administrator's own. The blanket switch ignores
scope options when it is off.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo swag
2026-11-03 21:55:38 +00:00
Ettore Di Giacinto 48b32619b5 docs(api): describe the host access routes and keep them out of the MCP tools
Add the host access routes to the /api/instructions text of the add-ons
group. Add a spec that no MCP tool maps to them and that nothing under
pkg/mcp names them, so an agent cannot reach them through a tool.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo
2026-11-03 21:55:38 +00:00
Ettore Di Giacinto c5400edd3f feat(addons): add the /api/addons/host-grants routes
Seven routes: read the host access state, preview an install, install or
upgrade with an approval, edit or revoke the grants of an add-on, change the
blanket approval and change the deny rules (with a dry run).

Reads take an administrator session or API key. Every write takes only the
administrator's own browser session: same-origin fetch and the confirm
header equal to the operation ID. An API key, bearer token, MCP session or
tool call gets 403 not_human_surface. The dry run of a deny change is a read.

The first path segment host-grants is reserved as an add-on ID, and the
routes are listed in the feature registry.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt ginkgo
2026-11-03 21:55:38 +00:00