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
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
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
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
StripPathPrefix and the redirect from / and /browse still read
X-Forwarded-Prefix from any peer, after BaseURL and BasePathPrefix
were gated on --trusted-proxies. A client could make the server strip
a path prefix or add one to a redirect.
Gate both on the same trusted-proxy resolver. The resolver is now
stamped into the request before StripPathPrefix runs, and the check is
exported as ForwardedHeadersTrusted. The explorer has no
--trusted-proxies option and keeps its old behavior by trusting every
peer for this one header. The docs say that the prefix needs a trusted
proxy.
Assisted-by: Claude:claude-sonnet-5-5 go-test
Show the host operations an add-on asks for in the install dialog, hold
an upgrade that asks for more until an administrator approves it, and add
a Host access tab with the grants of each add-on, the deny rules and the
blanket switch. All strings are in every locale.
Assisted-by: Claude:claude-sonnet-5-5 playwright eslint
Shape the answers of the host-grants routes for display, build the
approval that an administrator sends with an install or an upgrade, and
let the install flow carry it. A typed refusal keeps its type and its
details so the pages can say what happened.
Assisted-by: Claude:claude-sonnet-5-5 playwright eslint
Token help now works: a button opens GitHub's fine-grained token page,
the steps are open while there is no connection, and a table lists the
permissions for read only and for write modes. The page says it does not
answer mentions yet.
Errors sit under their own field. The repository list splits on lines and
commas only, so "not a repo" is one entry and is named in the message.
The connection name defaults to the first repository.
The write mode cards each carry one consequence line, and "Allowed" is
now "Acts without asking". The Test result is drawn per repository in the
row, with a tone, a busy line, and a plain text when GitHub refuses the
token. The unit tests and a browser spec cover the page.
Assisted-by: Claude:claude-sonnet-5-5 node playwright eslint
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
The Tools list of the agent form gained six grant rows, so the count is
14 and the Triggers switch needs an exact name. Two folder specs also
went to the edit page, which has no facts panel, instead of the agent
page.
Assisted-by: Claude:claude-sonnet-5-5 playwright
A binding needs a running add-on that serves its kind, so the real
server of the UI tests, which runs none, answers 400 instead of
storing an inactive binding. The spec now checks that answer and that
nothing leaks. Switching and listing stay covered by the add-on specs.
Assisted-by: Claude:claude-sonnet-5-5 playwright
The server renamed the group title, but the English locale kept "Web"
and the Tools menu reads the locale first. The menu, the home composer
and the agent form showed a name that the specs and the server did not.
Assisted-by: Claude:claude-sonnet-5-5 playwright
The Indonesian chat namespace was completed, so the ratchet of the
completeness gate failed the UI unit tests until the baseline went down.
Assisted-by: Claude:claude-sonnet-5-5 node
The trigger form has a Permissions section that lists the act.*
grants like the grants of an agent: a plain-words line on each,
sensitive ones marked, and a grant the chosen agent does not hold
cannot be ticked. It says in a callout when a run only reads and why:
no acts, the agent holds none of them, the proposal said read only,
or a read only connection made it. The details page shows the same,
the trigger list marks a read only trigger, and the proposal card has
a Permissions row, so the person sees the acts before pressing Create.
The editor always sends the acts it loaded, so saving a form never
drops them silently. New strings are in every locale.
Assisted-by: Claude:claude-sonnet-5-5 node playwright
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
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
An agent with no grant list keeps its non-sensitive connector features
(trigger tools, talk-back, files from senders). Granting any one built-in
makes the list the whole answer, so the others stop working. The editor
had no grants list at all, so nobody could see this.
Add a Permissions section to the agent editor with the declared grants.
When the first grant is ticked on a saved agent that had none, a notice
names the connector grants that would turn off and a button adds them.
Also fix the files.upload text. The gate enforces it on files a sender
attaches to a message, not on the agent sending files out, so the grant
is now "Receive files".
Assisted-by: Claude:claude-sonnet-5-5 go-test playwright
BaseURL and BasePathPrefix read X-Forwarded-Proto, X-Forwarded-Host,
X-Forwarded-Prefix and Forwarded from any caller when LOCALAI_BASE_URL
is not set. A client could pick the scheme and host that end up in the
SPA <base href>, generated links and redirects.
Gate them on the trusted-proxy resolver (--trusted-proxies): the headers
count only when the TCP peer is trusted. An empty list trusts nobody, so
the request's own scheme and host are used. LOCALAI_BASE_URL still wins
over everything for the origin.
This changes behavior for setups behind a proxy that rely on these
headers without setting LOCALAI_BASE_URL: they must now list the proxy
in --trusted-proxies. The docs say so.
Assisted-by: Claude:claude-sonnet-5-5 go-test
The speech-to-speech path labels its transcription WAV with the session
rate for client commits, so resampling first handed it 16 kHz audio under
a 24 kHz header. Resample only in the chained pipeline, and say so in the
docs.
Assisted-by: Claude:claude-sonnet-5-5 go-test
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
(cherry picked from commit 82cc411af54cf884e4a44a3e96292a323ba5611c)
The committed-utterance WAV is written with a fixed 16 kHz header. The VAD
commit path stores audio that is already resampled to 16 kHz (vadCommit),
but the client-commit path (input_audio_buffer.commit) passed the raw
buffer at the session input rate. For a 24 kHz OpenAI Realtime session the
backend decoded 24 kHz audio as 16 kHz (1.5x speed/pitch): garbage or empty
transcripts, then input_not_committed on the follow-up response.
commitUtterance now resamples from session.InputSampleRate to
localSampleRate, so both paths hand 16 kHz audio to the 16 kHz header.
This replaces the earlier approach of this PR (header at the session rate),
which mislabelled the already-resampled VAD audio (review feedback).
Tests go through the real paths with a 24 kHz session: commitUtterance and
a server_vad commit via vadTick; both must produce a 16 kHz WAV of the
resampled length. The client-commit spec fails without the fix.
Fixes#12747
Signed-off-by: nexxtmobile.de <git@nexxtmobile.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 05ba891da6)
(cherry picked from commit 56e2b6d0cc5cbf1944ef50735f7b499d998eb25c)
Clients written for llama-server, vLLM or SGLang switch template variables
per request with a top-level chat_template_kwargs object, most often
{"enable_thinking": false}. OpenAIRequest had no such field, so it was
dropped: with a Qwen3-style template thinking stayed on, the reply landed
in message.reasoning and content came back empty.
Fold the field's scalar values into the request metadata, so it takes the
path metadata already takes (gRPCPredictOpts, ResolveChatTemplateKwargs).
Booleans become "true"/"false", strings pass through, numbers become their
decimal string; nested values are skipped. An explicit metadata key wins.
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Cristi An <c4xp@msn.com>
(cherry picked from commit 2cd7425e87)
(cherry picked from commit 5e86ad6bd5286cc8db398d278adaa32e562223ca)
qwen3's chat template wraps <think> in {% if enable_thinking %}, but the
prefill detectors scan the raw template without evaluating the condition.
With streaming and no tools, the synthetic prefill made the Go-side extractor
classify the whole answer as reasoning whenever the client sent
metadata.enable_thinking=false.
Skip prefill detection when the request disables thinking. Real
<think>...</think> blocks that a model emits anyway are still extracted from
the content itself.
Fixes#12939
Assisted-by: ZCode:deepseek-flash
Signed-off-by: xianshujun <xianshupang@gmail.com>
(cherry picked from commit cee7ed4b32)
(cherry picked from commit 29618866d07a05624934bf9f595c95a68c938461)
Signed-off-by: Dedy F. Setyawan <dedyfajars@gmail.com>
(cherry picked from commit 2413f19b7d)
Conflict resolved against current master: keep the strings master already
has for chat.json and add the translations this change supplies for keys
master lacks. Keys added to master after the PR was opened are still
untranslated and fall back to English.
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
(cherry picked from commit 8d4799d91b33b2af204e7cc5386d8b984d09f04b)
A model can cite a plausible deep link it never opened, written from
memory. The client cannot tell it from a link a search tool returned,
because server-side MCP tool results never reach it.
evidence_check.urls compares every http(s) link of the final answer,
bare or inside a Markdown link, with the URLs of the request (system
prompt, system, user and tool messages) and of the request's successful
MCP tool results. Links are compared without scheme, fragment, "www."
and trailing slash; a link counts as retrieved when it equals an evidence
URL or is a shorter form of one at a path or query boundary. Unbacked
links (at most five) are listed in an appended note; urls_note replaces
the default note, with {urls} as the placeholder for the list.
Assistant messages are not evidence, so an invented link is marked again
when the client sends it back on the next turn.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 523e8cd3c4)
ChatEndpoint gained a compressor parameter after the spec was written, so
the openai test package no longer compiled.
Assisted-by: Claude:claude-sonnet-5-5 go golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The comment in sourceURLs named ExecuteMCPToolCall, which the tool
consumer matrix test (core/services/addons/tools) reads as a marker of a
file that discovers or executes tools. evidence.go only reads tool
output, so reword the comment instead of classifying the file.
Docs-Not-Needed: comment-only change, no behaviour change
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 1043d3cbc4)
Keep balanced parentheses in source URLs and decode MCP text arrays before
extracting links. This preserves Wikipedia paths and escaped query
separators instead of appending truncated citations.
Assisted-by: Codex:gpt-6
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 9b81cb20ab)
A system prompt that tells a model to look things up first does not stop
it from answering from its own knowledge, for example when the question
comes with a picture.
profile.require_tool makes the first model call of a chat request that
offers the profile's server-side MCP tools use tool_choice "required".
Every follow-up call of the server-side tool loop is made with the
client's tool_choice again: a "required" that stayed in place would make
the model call a tool after every tool result and never answer until the
iteration limit.
With a tokenizer template the backend enforces "required" with a grammar
built from the chat template's tool-call format. Where LocalAI builds the
tool-call grammar itself, the first call's grammar leaves out the
no-action function instead.
The first call is not forced when no server-side tool was offered, when
the client set tool_choice or function_call itself, or when the request
does not end with a user message, so a request returning a client tool's
result cannot bounce tool calls between client and server.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 2801938aac)
A research profile asks the model in its system prompt to cite its
sources, but a model can answer from a search result without naming a
single link, and the client never sees the server-side tool results that
held them.
evidence_check.sources appends one line with the distinct http(s) URLs
returned by the request's successful MCP tool calls (at most five, in
order of appearance) when the final answer names no URL itself. An answer
that already links something is left unchanged, and nothing is added when
no tool result holds a URL, so a link is never made up. The label is
configurable through evidence_check.sources_label (default "Sources") for
profiles that answer in another language.
It runs in the existing evidence gate, on the same streaming and
non-streaming paths as the number note.
Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 4ee6ff1395)
Four recent changes added English strings without the other locales, so
the locale completeness gate failed in the UI unit tests. Add the keys
to the eight locales. The baseline stays as it is.
Assisted-by: Claude:claude-sonnet-5-5 node
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
The Tools list gains a group, "What the agent may change", with one
row per builtin grant: its title and a plain-words description, and a
mark on the sensitive ones. The agent form switches them through
useAgentTools; a chat of a saved agent lists the grants the agent
holds, read only. Both draw the group with the same useToolGroups and
SwitchPanel code.
The group has no group setter, so Select all and Disable all, and the
"N of M on" count, leave it alone: a bulk click must not grant a
right.
Assisted-by: Claude:claude-sonnet-5-5 node
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
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
The draft tools and add-on lists, and the tools list of a stored chat,
read the execution status without an execution scope. The host logged
an error and reported the status as not selected. They now run in a
scope; a draft has no conversation, so its status says "draft".
In the Home composer, switching a built-in tool group on or off reset
the commands state to the loading value and never read it again. The
composer then showed the unknown warning for good. The Hide button also
did nothing for that state, because it needed a binding identity that
the loading value does not have. A draft now keeps the state the list
gave it, and Hide works for any notice that is shown.
Assisted-by: Claude:claude-sonnet-5-5 go ginkgo node
The notice came from navigation state, which survives a refresh, so it
came back after add-ons were on again. A frontend also reports add-ons
as off when the owner does not answer for a moment, which looked the
same as the instance switch being off.
Home now reads /api/features on mount, drops the notice and clears the
history state when add-ons are on, and clears the state on dismiss.
/api/features gains addons_enabled (the instance switch alone), so the
notice can say "not answering right now" with a retry button when the
switch is on and the owner is silent.
Assisted-by: Claude:claude-sonnet-5-5 eslint node go-test
A binding needs a kind now, so the route specs name the platform, and
the spec for an offline binding checks that nothing is kept while no
add-on serves the kind.
Assisted-by: Claude:claude-sonnet-5-5 go test
The add-on guide names one manifest api, one way to move a file and no
migration path, and drops the section on arguments of older tool
versions. The Swagger files are generated again, which also brings in
fields that the last generation missed, and the test of the apps view
now emits every field it documents. The inventory of the add-on suites
lists the specs by their new names.
Assisted-by: Claude:claude-sonnet-5-5 go swag
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
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
No release ever shipped add-ons, so there is no installed base on api 1.
The manifest must now declare api {"min": 2, "max": 2}; a manifest with
api 1, no api or a wider range is refused with a message that names the
range.
Every field that was gated on api 2 is now part of the contract, so the
Min*API constants and their gates are gone, and so are HostAPIMin and
the apiMin host field. Three older-manifest shims go with them: the
resource listing the host implied for one add-on, the legacyArgs hint
for arguments an older version of a tool took, and the tests that
checked the gates. All shipped manifests and the test fixtures declare
api 2.
Assisted-by: Claude:claude-sonnet-5-5 go test
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
Echo's RealIP trusted X-Forwarded-For and X-Real-Ip from any peer
when no IPExtractor is set. That fed the login and registration rate
limits and the API trace client address, so a client could dodge a
limit by rotating a forged header.
Add pkg/clientip, one resolver for the whole server. With
--trusted-proxies / LOCALAI_TRUSTED_PROXIES (IPs and CIDRs, empty by
default), the forwarded chain is read only when the TCP peer is
listed. The client is the right-most X-Forwarded-For entry that is
not a trusted proxy; a malformed entry stops the walk. With the flag
unset every request resolves to its TCP peer.
The resolver is set as Echo's IPExtractor and used by the access log
(--log-client), so all consumers agree.
Assisted-by: Claude:claude-sonnet-5-5 go gofmt
One manifest-driven guard replaces the Go-only provider guard. It reads
addons/*/manifest.json and fails when the Go strings of core and pkg,
the React UI source, the locale files or the core docs hold the id of an
add-on with a role (as a whole string or a tool-name prefix) or one of
its declared titles as a whole word.
Third-party mentions (OAuth login, release sources, token formats) and
ordinary words sit in a commented allow-list. The special cases that
remain (propose_change memory kind, the Account memory tab, the Telegram
conversion, the Tables listing shim) sit in knownDebt, each with the item
that removes it, and a ceiling that only goes down.
Reword the sandbox sentences in the container docs and use neutral
sample names in the UI catalog page.
Assisted-by: Claude:claude-sonnet-5-5 go test
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
The provider guard flags any core string that contains the name of a
shipped add-on that provides a generic role. The web add-on has the id
and title "Web", so the built-in tools group titled "Web" in the tools
picker (and the schema example for it) failed the guard on master.
Rename the group title to "Look things up online", which matches its
"cannot look anything up online" off-state text. Update the schema
example and the Playwright fixtures and specs that read the title.
Assisted-by: Claude:claude-sonnet-5-5 go test eslint
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
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
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