people_named_to decides whom the edit page and the share dialog name:
the reader, the holder's owner, anyone sharing a team with the reader and,
for the owner, whoever sponsored something on the holder. A resource's
owner, named as whom to ask or whose credentials a tool uses, also needs
the reader to see that resource. Sponsors, runs_as, contact and account
in resource_states and sponsors in sponsor_details all follow it; anyone
else is left unnamed and the pages say someone else.
A tool's run details (note, credential_mode, account, writes) come from
one function and only for a tool that runs. The share dialog keys each
person's own account on the per_user_account note, as the notice does,
and the unused noteKey helper and its string are gone.
Only removing a connection notes it on a kept tool. A config save through
update_tool or update_tool_config carries the stored note and ignores the
client's copy, so it can neither fake nor clear it; creating a tool, an
MCP server save and an agent import start without one. The note is now
written even for a connection with no catalog key, and a kept tool its
owner gave credentials of its own since runs again.
A member-mode tool runs on each caller's own account, so the owner's
account no longer marks it stopped: it is active with the note
per_user_account, and only an admin turning its service off stops it.
connection_removed now needs the note remove_connection leaves on a kept
tool, so a tool that never had a connection (a tokenless ntfy) runs.
Whom to ask is contact_role resource_owner; contact names them only when
the reader can see the resource or they own the holder. connection carries
its id only with can_reconnect and the account's own name only for its
owner. Run state and audience are best effort (a failure logs and leaves
them out of the read), resolve answers are cached for the rest of a read,
and a workflow read names node resources by the run state's own names.
resource_states gains runs_as, the live sponsor a running item runs as
(None once the owner can use it, even with a sponsor on record), and
writes_allowed, False when an admin turned off changes through the
tool's connector (its writes are then not listed for the allowlist).
account now also names the owner of an API tool's saved key, a signed-in
MCP server or stored secrets, not only of an owner-mode connection, and
only to a reader who shares a team with that person. A running tool's
connection id is no longer sent.
resource_states now carries, for a running tool, its credential mode when
it has a connection (after any mode an admin forces), whose account an
owner-mode connection acts as, and the write actions it takes on
credentials its owner stored, which API, widget and public-link users can
run only from the agent's API write allowlist. The service of a connected
tool is named whether it runs or not. Still only for people who may edit
the agent or workflow.
The agent and workflow reads now return resource_states to people who may
edit them: every attached tool, source and prompt (and workflow node tool
and source) with active or stopped and the reason: deleted,
owner_lost_access, the sponsor reasons, connection_needs_reconnect,
connection_removed or connector_disabled. Each entry names the sponsor,
someone other than the reader who can fix it, the service for a connection
reason, and whether the reader may take it over or reconnect it. When
something can be taken over, sponsor_audience says who it would reach.
The state comes from the checks the run itself uses (ref_access,
resolve_holder_tool and the tool's connection as the run resolves it), so
the page and the run can't disagree. A run that leaves a resource out logs
resource_stopped with the holder, type, id and reason.
The workflow read gives sponsor details, run state and node resource names
only to people who may edit it, and names only resources the workflow runs,
someone sponsored, or the reader can see. Owner saves and new workflows
now refuse node tools and sources the owner can't use, like editor saves.
A resource attached in a save now ignores any sponsor recorded for it
before it was removed: the caller must be able to sponsor it and confirm.
YAML import prunes the sponsors of resources it drops, for agents and
workflow graphs. Sponsor details, with the resources' names, go only to
people who may edit the agent. The workflow read returns the names of
every node tool and source, so editors can remove the owner's private
ones. An agent image is stored only once the save is known to go ahead.
Research steps ran each tool call straight through the executor, never
asking check_pause, so approval-gated actions (including a public-link
visitor's wiki edits) and an outside caller's writes on the owner's
accounts ran unchecked. A step can't pause, so it now checks each call
first and answers any pause with a refusal the model reads, journaled
like a headless denial. Headless and research runs share the journaling.
A paused turn is found by the agent owner's id, so anyone holding one of
the owner's agent keys could resume the owner's own chat with its saved
wiki edit rights. A resume now counts as an API or widget caller when
either the saved state or the resuming request is one, cuts the wiki tool
to wiki_view unless the wiki allows outside edits, and gives the tool
executor the same flags. A request that names an agent, by key or id, may
only resume that agent's turn; otherwise the claim is released and the
request refused.
Public-link visitors run as themselves and reach only wikis they may edit,
so the wiki switch no longer applies to them. Instead every wiki write in
a public-link run waits for the visitor's approval, so the agent owner's
prompt or sources can't steer an edit to the visitor's wiki unasked.
Sponsoring a tool, source or prompt the agent's owner can't use now takes
owning it or having edit access to it; use access alone no longer extends it
to the agent's audience. A save that would make the caller a new sponsor is
refused with 409 sponsor_confirmation_required (the resources and the
agent's audience) until it is retried with confirm_sponsor listing them.
When a sponsor loses access, the resource stops instead of passing to
whoever saves next; another editor takes it over only by confirming.
Workflows follow the same rules. sponsor_details now reports each
sponsorship's state, the reason it stopped, and whether the reader can
take it over.
A run from an agent's API key or widget acts as the agent's owner, so it
could rewrite any wiki the owner can edit. A new per-wiki setting,
wiki_outside_edits (off by default), decides whether such runs, and runs
from the agent's public link, get the wiki's edit actions. While it is off
they are offered only wiki_view, and the tool refuses writes itself after
reading the live setting. The owner changes it through the owner-only
/api/sources/<id>/wiki/settings route; tokens can read it but not change it.
It covers writes on any credentials the owner holds, for API-key and
widget callers, public-link users and the schedules they set, not only
connected accounts reached with the API key.
The owner sets a webhook up and its URL is a secret, and a webhook run
already refuses every action that needs approval, so holding it to the
API write allowlist only broke the owner's own automations. Webhook runs
no longer count as external callers; schedules keep their caller rules.
The allowlist copy no longer names webhooks.
Every API tool counted as holding the owner's credentials, so outside
callers were refused any write on one even when it sends nothing the
owner stored. Now an action counts only when its headers or query
parameters carry a saved value (sealed or legacy plaintext) or the tool
stores credentials; the allowlist lists just those writes.
The answer routes refuse such a request with 401, but only after building
the agent, so a public agent's prompt tools were pre-fetched, and their
actions run, for nobody. Anonymous chat without an agent key is not
supported, so the processor now stops before any setup and the route
answers 401 as before.
A scheduled or webhook run acts as the agent's owner with no one to
approve, so a public-link user or API-key caller could have the agent
schedule a write and have it run on the owner's accounts. Runs now keep
the caller's rules: a schedule set by someone who reaches the agent only
by its public link runs as a public-link caller, one set through the API
(recorded as created_via 'api', migration 0042) and every webhook run as
an external caller, each with the agent's API write allowlist.
Pre-fetch judged someone else's tool by its stored approval flags, which
a remote device or the code executor decides per call, and it took a
widget or API run for the owner because the run carries the owner's id.
Those tools no longer pre-fetch for anyone but their owner, an API-key or
public-link run treats every tool as someone else's, and writes with the
owner's credentials are never pre-fetched for them.
API-key, widget and public-link callers were held to the write allowlist
only on connected accounts, so they could still write through an API
tool or a signed-in MCP server that carries the owner's credentials. Any
write on credentials the caller doesn't hold is now refused unless the
owner allowlisted it, and a scheduled run for such a caller counts as not
holding any. The tool list names these writes per tool, so the allowlist
can offer them, and its copy now names every route it covers.
The /v1 route runs with the agent owner's token, so the processor took a
key holder for the owner: writes on the owner's connected accounts ran or
waited for an approval the client could send. The route now marks the
processor as an external caller server-side, which keeps the allowlist
in force for the run and any resume, including state saved before.
Callers that build a StreamProcessor without __init__ and stub the agent
key lookup never set the flag, and configuring the agent then failed.
It now reads as not a public-link caller.
Headless runs searched only the agent's primary source, so an agent whose
knowledge sat in its extra sources answered a schedule or webhook without
it. They now take the primary and every extra source through the same
owner-or-sponsor check a chat uses, shared as one helper, and retrieve
through the per-source dispatcher so each source keeps its own settings.
Tool pre-fetch ran the caller's own active tools, so a teammate chatting
with a shared agent had the owner's prompt fill in from their tools, and
even the owner got every active tool rather than the agent's. It now uses
the toolset the run gets: the agent's tools as its owner or sponsor, or
the caller's tools and defaults outside an agent. Pre-fetch asks nobody,
so on someone else's tool it skips approval-gated actions and anything on
a connected account.
Someone who reaches an agent only through its public link was offered the
approval card for writes on the owner's connected accounts, so a stranger
could approve for the owner. Those writes are now refused with a tool
result, like an API-key caller's, unless the owner allowed the action in
the agent's Access details. Team members keep the card, and a tool on the
caller's own account (member mode) is unaffected. The flag survives a
resume, and workflow nodes now follow the run's caller rules (scheduled,
API-key and public-link) instead of starting from none.
A node's tools resolved as the person running the workflow, so a teammate
or public-link user lost every owner tool they could not use themselves.
They now resolve as the workflow owner, then as the editor who attached
them, like an agent's own tools. The runner stays the invoker, so a
member-mode connection still uses their own account.
Member-mode resolution ranked accounts by last_used_at first, so a
connection added by Connect to continue (never used) lost to any older
used account. Accounts now rank by the later of last use and creation.
The read fallback matched read verbs as substrings, so update_spreadsheet,
set_budget and enlist_member ran as reads with no approval. Names are now
split into words, and any write verb makes the action a write.
Migration 0040 dropped token_info values that were not objects before
clearing the plaintext column. Objects stored as JSON strings are now
decoded, and other values are kept in the envelope under
legacy_token_info so downgrade restores them.
Removing a connection with its tools now also deletes those tools' sharing
switches in the same transaction, as deleting a tool on its own does.
Team grants and chat preferences were already removed with the tools.
At run time a tool now refuses a connection whose connector does not list
the tool's type (a Telegram bot token never reaches an ntfy or MCP tool),
and an MCP tool refuses a connection when no server is known for it,
instead of sending the key without checking. Legacy MCP rows still find
their server from the stored provider value.
/api/update_tool now checks submitted actions against the stored ones the
same way /api/update_tool_actions does: nothing can be added, and only the
tool's owner can change a fixed value. The tool type (name) can no longer
be changed after creation by anyone.
For API tools, changing who fills an existing header, query or body
parameter, its value, or clearing a stored value is now owner-only on both
/api/update_tool and /api/update_tool_config. Before, an editor could hand
a stored secret query value to the model and read it back in the chat.
The chat now masks every value that came from the stored action rather
than from the model.
A connection-backed MCP tool can no longer be moved to another server or
sign-in method through /api/update_tool or /api/update_tool_config (400,
pointing to /api/mcp_server/save), and a new key saved through
/api/update_tool_config goes to its connection, owner only.
Main's access model (team editors and viewers, edit_credentials, owner-only
OAuth servers, per-user tool preferences, resource sponsors) now applies to
connection-backed tools and sources. Our migrations are renumbered to
0040_connections and 0041_connection_account_name, after main's
0038_resource_access_settings and 0039_resource_sponsors.
Where the two sides met: MCP tools save and load their connections as the
tool owner, editors may add a key (a new connection on the owner's account)
but never rewrite an existing connection's secret, fixed values stay
owner-only, and a member's own connection is used in member mode.
After removing a Linear connection, connecting again could skip signing in
and save Linear as an unconnected custom tool: a client cached before the
removal still held the old tokens, and a late token write re-created a
connection for them. A token write for a named connection no longer creates
one, removing or disconnecting a connection drops its cached clients, and a
sign-in server is never saved without its connection.
The connection setup endpoint takes sync.config, validated like an
upload's config, and passes it to the ingest task so the synced source
gets the chosen chunking and retrieval settings. An invalid config is
refused before the idempotency key is claimed.
An issue deleted after it was listed, or comments the token cannot read,
now lose that detail with a warning instead of failing the whole sync; an
unreadable document is skipped.
The chunker caches only a model's tokenizer.json in the embeddings cache.
FastEmbed counts any cached snapshot as the model, so it never downloaded
the ONNX graph and every load failed with NO_SUCHFILE. The loader now
checks the files FastEmbed needs and fetches them first when they are
missing; offline it leaves them for FastEmbed to report.
A value the owner fixed may be a secret, and tool-call events reach whoever
runs the agent, so query and body values the owner fixed now show as
(fixed); a value the connection sets, like Telegram's default chat, still
shows. sent_arguments is saved with the conversation, so a reopened chat
shows the same arguments as the live one.
The Linear connector now syncs as well as giving agents its tools, from
one connection. Linear's MCP server is its own OAuth issuer, so its
tokens are read through the same MCP tools the agents use (list_issues,
get_issue, list_comments, list_documents) rather than Linear's GraphQL
API, and no OAuth app has to be registered.
A source picks teams and projects, with comments (on by default) and the
projects' documents. Each issue becomes one document with its state,
assignee, priority, labels, description and comments, filed under its
team and citing its Linear URL. Each sync reads up to 500 issues and 100
documents again. /api/connections/<id>/linear lists the teams and
projects to pick from. Sources sync on their schedule with the owner's
connection, and pause when the sign-in needs reconnecting.
run_connection_session opens one MCP session signed in with an MCP OAuth
connection's stored tokens and runs a batch of tool calls in it, so a
sync makes hundreds of calls over one sign-in. The tokens only go to the
server the connection signed in to. A sign-in that expired and cannot be
renewed flags the connection for reconnecting, which pauses its sources;
network trouble and rate limits are reported as worth retrying.
The MCP SDK knows a token's expiry only when it obtained the token in the
same process. A worker or a restarted API loading tokens from the
connection sent an expired one, got a 401 and asked the owner to sign in
again. The expiry is now saved with the tokens and handed back to the SDK,
which then renews the token with its refresh token first; a token saved
before expiries were kept is renewed once. A sign-in that cannot be
renewed raises MCPReauthorizationRequired.
A GitHub connection's MCP tool can now point at GitHub's full endpoint
(/mcp/) instead of the read-only one when its owner opts in, at setup
(allow_writes) or later (PUT /api/connections/<id>/writes), which re-reads
the actions and keeps the choices for those on both endpoints. Actions from
the write endpoint are writes unless GitHub marks them read-only, so they
default to asking first.
Admins can forbid it per connector (allow_writes in Admin > Connectors,
kept in app_metadata). Then the option is refused, a refresh goes back to
read-only, and at run time the tool only ever calls the read-only endpoint
and write calls are denied with a reason.
The approval card and a finished call showed what the model asked for,
even where a fixed value replaced it. Calls now carry sent_arguments for
the chat, leaving headers out since they may hold a fixed secret. The
model's own arguments are kept as they were, because they are what later
turns replay to it and it never sees fixed values.
Connections get an account_name (migration 0039) that the owner sets with
PATCH /api/connections/<id>; the account label stays the account's
identity, so signing in again still finds it. When someone has more than
one account of a service, its tools are listed as "Telegram · <account>"
and the model sees each account's actions under the account's name
(telegram_send_message_alerts_bot) with the account in the description,
instead of _1 and _2. Actions shared by different services are named
after the service. Tools a user renamed keep their name.
The connect form shows field hints; Telegram's Default chat ID says what
it does and how to find a chat's id. In the drawer, a chat set on the
account shows as set there instead of as a choice.
A Telegram connection can now hold an optional default chat ID. When it
is set, both Telegram actions send there, the model is no longer asked
for a chat, and a chat it names anyway is ignored. In member mode each
member's own connection supplies their own chat. The same bot with a
different chat is a separate connection.