Commit Graph
518 Commits
Author SHA1 Message Date
Ettore Di Giacinto 5df6818bb8 fix(addons): fail closed on the apps list when the owner does not answer
A frontend kept the last apps list per person and answered 200 with a
stale mark when the owner timed out. Two frontends then disagreed: one
with a cached list answered 200, the other a typed 503. The acceptance
contract is that every frontend gives the same typed answer when the
owner cannot answer, so the server cache is gone.

The route now answers owner_busy (timeout) or owner_unavailable with a
503 from every frontend. An owner that is up and busy still answers
from its snapshot, with the operation of each add-on in the list. The
Add-ons page keeps the last list in the browser and shows it with the
busy notice, so it never goes empty. The specs, the docs and the
swagger spec follow the new contract.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 20:00:36 +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 9da4a1dd62 fix(studio): stop background work before shutdown and wait for tool results in specs
The connector spec "keeps asking when the setting is ..." read the tool
results as soon as the bot had an answer. The harness records a result
after it emitted the turn events, so under load the answer could come
first and the list was empty. The spec now waits for the result.

Shutdown left two background writers running: the file text indexer loop
ran on the application context, and the chat search backfill had no stop.
A test that removed its temp directory right after Shutdown could meet a
write into the "textindex" folder ("directory not empty"). The indexer
loop now has its own context and Shutdown waits for it, and the chat
store gets a Close that stops the backfill and waits.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 19:02:54 +00:00
Ettore Di Giacinto 912fb8ab03 test(memory): list the memory choice among the names the guard allows
The memory choice of an agent names the built-in memory in the agent
form, the notes list and the agent delete. The add-on name guard flagged
those four lines after the previous commit.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 18:26:41 +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 f52f10c7b0 fix(studio): name the file store search engine scan, not memory
The provider guard test refuses core code that names an add-on, and
"memory" is the name of one. The file store searched its in-memory
transcripts and reported the engine as "memory". It now reports "scan",
as every store without an index does.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 18:01:50 +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 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 82b9984e06 feat(memory): serve the notes of memory from the host
The Memory add-on only declared three index files, two skills and a save
setting; the host did the rest. Without the add-on a memory proposal
worked but nothing was read back, and removing the add-on silently reset
the save level.

The host now loads the three indexes (user, agent, conversation) with
the same caps and data wrapping, serves context_read, ships the two
skills as built-in skills with the same text, and keeps the save level
as its own per-person setting (ask, auto, never; default ask). The code
sits behind a small MemoryProvider interface.

The add-on stays installed and harmless: its index files and skills are
ignored while the host serves memory, so nothing loads twice. A person
who chose a level on its page keeps it until they choose in the host
setting; auto still needs the add-on to run. A person who disabled the
add-on and never chose keeps memory off. Saved agents still read notes
only when their add-ons list names memory, and messaging chats still
ask before saving.

A person with no note and no add-on gets no extra block in the prompt.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 17:25:40 +00:00
Ettore Di Giacinto bab8a605cc feat(addons): add the PiKVM add-on
Chats and agents can check a PiKVM (kvmd) device and, when a person
confirms each call, press the power buttons of the machine it controls
and use its keyboard.

Read tools: get-info (model, versions, no serial or host name),
get-power-state (ATX LEDs) and get-keyboard-state. Write tools:
power-action (on, off, reset; off_hard, reset_hard and long_press only
with a per-device switch), type-text (bounded, no control characters,
refuses key, token and password-list shapes) and send-key (a short
allowlist, no Windows or Meta chords). All three declare approval
always. The preview reads the power state; apply reads it again and
rejects a stale operation. The add-on journals before it sends, never
retries and reports unknown when the connection drops.

One connection is one device: the address is the single resource of the
connection and the one sealed credential holds user:password, split at
the first colon. A one-time code is not supported. The add-on speaks
only to that address, refuses redirects, uses no proxy and accepts a
self-signed certificate only through a #sha256= pin in the address.
Typing, the text length and hard power actions are per-device switches
that an administrator sets on the add-on page; all start off. Snapshots,
mass storage, GPIO and the mouse are left out.

The add-on ships a skill, a connection page with a Test button and
approval cards, the package files, the library entry and the docs, and
joins the package workflow and the case inventory.

PiKVM API reference: https://docs.pikvm.org/api/

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-04 17:20: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 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 7abbef8236 feat(addons): wire the Discord add-on into packaging, catalog and docs
List the add-on in the package workflow matrix, the package checks, the host
operation policy, the catalog template and index, the case inventories of the
contract and packaging suites, and the architecture guard (two links to the
project community server are not the add-on). Document the connector in the
chats guide.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 15:01:17 +00:00
Ettore Di Giacinto 68438344d3 feat(addons): add the Jellyfin add-on
Let chats and agents query a Jellyfin media server and, when the person
allows it, remote control its players. Read tools: search, libraries,
browse, item details, latest, continue watching, next up, playlists,
sessions, user names and server info. Write tools: control-playback
(play, pause, resume, stop, seek, volume), mark-played, set-favorite and
add-to-playlist. There is no tool that deletes, scans or edits the
library or manages users.

One connection holds one API key (credential kind token) and one server.
The server address is the single entry of the connection's resource
list, so the host checks it before every call and several servers work
through the connection selector. The add-on talks only to that address:
http and https only, no user information or query, redirects refused,
paths built from fixed segments, and the key sent in the Authorization
header only. A self-signed certificate is accepted only when the
address carries #sha256=<fingerprint>; the connection then accepts that
one certificate and TLS checks are otherwise on.

Results are bounded (50 items a page, cut overviews), marked as
untrusted data, and hold no file path, stream or image address. The
write mode off, propose or auto is the host's per-connection setting.
A write is previewed from the server's own records and rejected as stale
when the player or playlist changed before the person confirmed.

The add-on ships a skill, a connection page with a Test button and
approval cards, the package files, the library entry and the docs, and
joins the package workflow and the case inventory.

Assisted-by: Claude:claude-sonnet-5-5 go node
2026-11-04 14:55:15 +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 2b9f62d055 fix(studio): label thread lines for the asker and the bot
The block of earlier thread messages listed the asker's own earlier
messages under their name, inside a block that said it held messages
from other people. The model then said it could not see what the asker
wrote. Lines of the asker are now "The person asking (Name)", lines of
the bot "Assistant (you)", and the heading says the block holds the
earlier messages of the thread, including those two, as untrusted text.

The asker is matched by platform id, then user name, never by display
name. A name that imitates a fixed label is shown as "Someone (...)".

Each thread read now logs one INFO line with counts only: messages
returned, lines kept, bytes, truncated, present or empty. A kind that
serves no thread history logs a distinct line.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 13:44:22 +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 23594dcc60 test(studio): widen the margin of the turn reap spec
The spec "a turn that stops beating is reaped, one that keeps beating
is not" beat the live turn once and then made several store calls
before the second reap with a 50 ms threshold. Under -race on a loaded
runner those calls took longer than 50 ms, so the live turn counted as
stale and the second reap found it.

Use a 300 ms threshold, sleep twice that for the stale turn, and beat
the live turn again right before the second reap. The assertions keep
their meaning and the store is unchanged.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 11:55:11 +00:00
Ettore Di Giacinto 19a0e6ecf1 test(studio): cover the app tool call time limit below the e2e suite
The platform e2e spec "ends a tool call that outlasts the 60 second
limit" has failed on master since connector tool calls got the MCP tool
timeout (360 s by default) instead of the connector package default of
60 s. The fixture tool sleeps 90 s, the call is no longer cut at 60 s,
and it returns 200 with "slept". The new limit is intended, and a
standalone server has no flag to shorten it, so the spec can only pass
by waiting more than six minutes.

Move the check to the studio service tests: a connector service with a
one second call time limit, and an app tool that sleeps past it, must
end with ErrMCPAppFailed and no credential in the text. Remove the e2e
spec.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 11:38:51 +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 ceecea147d test(studio): the allowlist does not pass the gate of an auto trigger that acts
The gate behind the auto start of a trigger with acts asks about a
capability that is not sensitive, with the Sensitive mark. Pin that an
allowlisted sender alone is refused and a paired one passes.

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 a1157fb811 Tell the sender when an answer shows a file that was not attached
A model could write a paperclip line with a made-up file name and
never call attach_file. Nothing was sent and the sender got no note.

When the answer of a connector turn has a paperclip followed by the
name of a sendable file, and no file went and no other note was
sent, the host now adds one plain note. The answer is not changed.
Prose that names a file and a paperclip alone do not match.

The attach_file description and the connector guidance now say to
use the exact path the tools showed, and never to write a file name
or a paperclip line by hand. A spec covers a folder name with a
space.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 09:14:18 +00:00
Ettore Di Giacinto e6fe327203 fix(studio): let a binding read the history of its thread
AuthorizeConnector had no case for the op history, so the default
branch refused it. A mention in a Slack thread then logged "connectors:
forbidden" and the first turn of the new chat got no thread block.

History now follows the rules of a send: the binding is the whole
authority, the token comes from owner storage and a disabled binding
keeps it. Only the poll stops with the binding.

The studio specs ran over a transport that skipped the scope check, so
none of them saw the refusal. The rig now authorizes every call with the
real manager, and a new spec table pins each op of the contract.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 09:12:09 +00:00
Ettore Di Giacinto 81a56608d1 test(studio): wait for the answer of a talk-back turn, not its working message
The turn helper of the talk-back specs ended its wait on the first text in
the chat. That text is the working message that goes out when the turn
starts, so a spec could read the tool results before the tool had run.
The refused-channel spec failed that way under load and the race
detector. Count only the replies that are not the working message.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 08:00:34 +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 8857fd36f4 test(studio): wait for the delivery row of a trigger report
The text of the report reaches the channel before the access row is
written, so reading the rows once right after the text was a race.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 07:01:13 +00:00
Ettore Di Giacinto 1e75fc11bc test(connectors): wipe the migration and pairing tables before each PostgreSQL spec
The coverage run and the no-skips run share one database. The migration
claim left its rows behind, so the second run found the claim taken and
two specs failed. Wipe the two tables the setup left out.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-04 06:59:46 +00:00
Ettore Di Giacinto e351118f85 fix(addons): deny a stale generation before the credential check
A read of an add-on that declares credentials looked up its capabilities
before it compared the generation. A stale generation made that lookup
fail, and the failure was reported as an outage of the owner (503)
instead of a denial (403). Classify the lookup error like the tool check
does.

Assisted-by: Claude:claude-sonnet-5-5 go-test
2026-11-04 00:09:39 +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 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 c604f3a133 feat(addons): install, upgrade and downgrade from a staged zip file
The manager takes a staged file as a release: sourceId local-file and
the file ID as the version. It runs through the one lifecycle path, so
generations, backup, recovery and the host access approval apply as for
a library package. The approval digest is the sha256 of the zip.

The same version is refused, and an older one needs a confirmed
downgrade. Files wait in a private per-user staging area for an hour.
An add-on from a file has no library, so no update check answers for it.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto e6ff1ab0f9 feat(addons): stage a package from a local zip file
Add Installer.StageFile and InspectFile. A zip is checked against the
digest manifest it carries, refused for unsafe or duplicate paths,
links, wrong executable bits, too many or too large files and bomb
ratios, then staged through the same inspection as a library package.
The receipt records the source local-file and the origin "local file,
unsigned".

Stage now shares its manifest, executable, UI, icon and skills checks
with StageFile.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:50 +00:00
Ettore Di Giacinto 2140e56aa5 fix(addons): keep grants made under a deployment blanket as the install's own
The document validator requires a blanket grant to name a revision of the
switch in the document. A deployment-fixed mode is never written there,
so its grants have no revision to name. Record them as the grant of the
install or upgrade that made them; the audit line still carries the
blanket tag.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 22:52:09 +00:00
Ettore Di Giacinto 3ca176d15d feat(addons): lock UI host grants while sign-in is off
With authentication off every caller that reaches the port is the same
local owner, and the checks that tell a person from a script are
headers the caller chooses. Any process or agent that could reach the
port could grant host operations, set deny rules and turn the blanket
approval on.

UI host grants are now locked by default in that mode. The policy file
and flags still grant, and every write route answers 403
locked_by_deployment with a sentence that says why. The overview and the
preview carry the lock and its reason for the page. The new flag
--addon-host-ui-grants-without-auth turns the UI grants on knowingly and
logs a startup warning. With sign-in on nothing changes.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:09 +00:00
Ettore Di Giacinto 60f67856da fix(addons): hold a package from another library or signer, and fail an uninstall that leaves grants
The UI grants of an add-on name the storage and the add-on ID. A plain
upgrade through dispatch could resolve to a package from another
configured library or signer and inherit every grant without a person
looking at it.

An install or upgrade without an approval is now held when its package
comes from another library or signer than the installed one, like an
upgrade that asks for more operations: the old version keeps running
and the refusal says to approve in the browser. A request that carries
an approval for the exact digest goes through.

Uninstall deleted the grants after the journal commit and only logged a
failure, so a reinstall under the same ID before a restart could meet the
old entry. The grants are now deleted before the commit, and a failure
stops the uninstall with the add-on still installed.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
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 b8823e91d1 fix(addons): re-sync the manager after a host grant change that removes access, and audit before writing
Revoke, an edit that only removes, a deny rule change and a blanket
switch-off with revoke never asked the manager to look again, so an
add-on that lost a required operation kept running, and one that was
refused for a deny stayed refused after the deny was lifted. Each of
these routes now calls the refresh hook, as the grant routes did.

The audit lines of these writes were discarded on error, so a grant
could exist without its record. Store.UpdateAudited runs the audit
append after the patch is validated and before the document is
written. When the append fails nothing is written and the route
answers 503.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:09 +00:00
Ettore Di Giacinto 65f8116b1f fix(addons): keep a deployment blanket mode out of the grants document
Stage copied the blanket mode that a flag fixes into host-grants.json
at the first install. After the flag was removed the document still
said "any", so the switch stayed on without anyone typing the
confirmation text.

The deployment mode is now applied at read time only. The document keeps
what a person set, and a grant made under the flag records revision 0,
which the validator accepts for a blanket grant of the deployment.

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