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.
The table adds tools with saved credentials, says API and widget users
get the owner's account on a tool each person connects, that a sponsored
item runs as the owner again once the owner can use it, and that a
teammate's name shows only to people who share a team with them. The
editor switches are named per resource type, public-link users are said
to be asked only for writes that need approval, the allowlist is said to
cover sponsored and workflow node tools, and the badge names and the new
resource_states fields match the app.
A new "Sharing agents and what they use" section covers viewers and
editors, whose access each tool, source and prompt runs with as the share
dialog labels it, sponsors, stopped resources, what API, widget and
public-link users can't do without the write allowlist, the wiki switch
and research steps. The sharing rules now mention the editor switches and
member-mode tools, the guardrails page no longer says editors can't
change guardrails, the connector guide uses the new tool share labels,
and related pages link to the section.
Public-link visitors edit only wikis they can edit themselves and approve
each edit. The docs also note that with authentication off every caller
is the owner's local user, so the switch has no effect there.
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.
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.
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.
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.
Explains the opt-in switch, the full endpoint it uses, how write actions
default to asking first, the admin's Write access switch, and which
fine-grained token or GitHub App permissions changes need.
How members connect with a fine-grained token or Sign in with GitHub, how
an admin registers the GitHub App (callback URL, permissions, requesting
authorization during installation, settings), and that
GITHUB_ACCESS_TOKEN now reads public repositories only.
One GitHub connection feeds both a Knowledge source and an agent tool.
Users connect with a personal access token, checked against GitHub and
named after the account, or, when an admin registers a GitHub App
(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_APP_SLUG), with Sign in
with GitHub. App tokens expire after eight hours and are refreshed before
each sync or tool call; tokens with no expiry are never treated as expired.
The catalog gains an optional second sign-in method (oauth_settings,
exposed as sign_in_methods) without changing any other connector.
Sync lists the repositories the connection can read (the token's own, or
the App installations') and ingests one with the connection's token. The
tool is GitHub's read-only MCP server (api.githubcopilot.com/mcp/readonly):
setup discovers its actions and creates it bound to the connection, and
the executor sends the connection's token only to that server.
- Tools from a connection turn on and off on the connector's page (a
real switch, not a badge), next to their action permissions.
- Knowledge synced from a connection lists its sync state there, with
Sync now (connector sources and S3/Reddit sources each through their
own endpoint).
- On the Tools and Knowledge pages, anything from a connection has
"Manage connection" in its menu, which opens that page; the tool
editor with its second credentials form is no longer offered for them.
- A link to a part (Jira & Confluence) opens its parent's page.
- Sources (the collection) are called Knowledge: the settings page (now
/settings/knowledge; /settings/sources redirects), the composer and
agent pickers, Add knowledge, and "syncs into Knowledge" on connectors.
Citations keep "sources". All seven locales.
- Connector cards describe what they do ("Syncs into Knowledge", "Looks
things up", "Takes actions") and drop publisher labels ("Built in",
"Preset"). Filters read "Files & storage" and "Docs & wikis" so
"knowledge" means one thing.
- Custom tools are tools: "Add custom tool". The agent picker no longer
splits built-in tools into "Built-in" and "Default".
- Confluence is one card: syncing pages into Knowledge and the Jira &
Confluence MCP actions (part_of on the preset) share a page. The part
shows on its own when Confluence sync is not set up.
- One control for members' own MCP servers: the custom MCP server row's
switch (it sets both the policy and the instance switch); the separate
top-level switch is gone.
- Connectors that only sync content show "No tools" instead of a
sharing policy, and the policies read "The sharer decides per share",
"Always the sharer's account", "Always each person's own account".
- A connector that needs server settings can't be switched on until they
exist; a tooltip says why.
- On phones the table becomes a list ("On · 2 connections · …") and each
connector's controls open in a sheet.
Notion, Linear, Atlassian, Sentry, Asana and Stripe no longer open the
MCP server form. The wizard shows one "Sign in to Notion" button: the
pop-up opens inside the click (so browsers allow it), follows the
worker's mcp.oauth events to the provider, and the tool is saved on
success with its actions on the done step. Reconnecting a preset uses
the same flow, as do the chat bar and the health toast.
Saving an OAuth MCP server without a new handshake now uses the stored
sign-in instead of failing, and the custom MCP form keeps scopes and
timeout under Show advanced.
An agent called with its API key (widget, API) runs as its owner, and
nobody can approve an action there, so a write set to Always allow ran
on the owner's account for anyone holding the key. A caller is external
when the request carries the key and is not signed in as the owner (an
owner previewing their agent keeps full access).
For external callers, write actions on connection-backed tools are
refused unless listed in the agent config's new api_write_allowlist
(tool_id:action), and a missing connection is refused instead of pausing
on a Connect card the widget cannot show. The flag and list survive a
paused-and-resumed stream. Owners pick the allowed actions under Access
details; a team editor's update keeps the stored list.
A connector's enabled switch is now optional: unset means on when the
connector has the server settings it needs, so Google Drive, SharePoint
and Confluence start off until their OAuth settings are present, and an
admin's explicit switch always wins. Changing only the sharing mode no
longer switches a connector on.
Members only see connectors they can use. The catalog, Add Source tiles
and Add Tool leave out anything turned off or still needing setup, except
a connector a member already has a connection to, which stays listed as
turned off so it can be managed or removed.
- Guides > Connectors: using connections, sharing modes, attribution,
and admin setup (encryption key and rotation, redirect URIs, Google
Drive, SharePoint, Confluence, S3, MCP presets).
- Integration pages register CONNECTOR_REDIRECT_BASE_URI as-is; the
?provider= suffix never matched what the backend sends.
VITE_GOOGLE_CLIENT_ID is optional.
- Upgrading: set ENCRYPTION_SECRET_KEY before migrating multi-user installs.
Adds a v2 credential envelope next to the v1 tool-secret helpers:
AES-256-GCM, a master key derived once per process from
ENCRYPTION_SECRET_KEY, and a per-record key from HKDF over the owner's
id, which is also the associated data, so a blob moved onto another
user's row does not decrypt. The envelope names its key, so
ENCRYPTION_SECRET_KEY_PREVIOUS keeps old rows readable during a
rotation.
Log redaction now also covers token_info, tokens and client_info, and
the API warns at startup when the public default key is in use.
Covers what is recorded, where it is shown, the TRACES_* settings, the
gen_ai.* spans and metrics, content capture, and the limits of exporting
spans when the request ends.
TRACES_* settings for the per-request trace timeline and its GenAI OTel
export. opentelemetry-api was only transitive; the tracing package
imports it directly.
The DEFAULT 'unknown' added in the last pass stopped a previous-release
process from raising NotNullViolation mid-rollout, but it threw the
attribution away to do it. The highest-volume auth_events writers are OIDC
login and silent renewal, where the actor is simply the user, so a rollout
window would have flattened exactly the rows that were trivially recoverable.
Lifts both derivation rules into SQL functions -- auth_events_derive_actor
and auth_events_derive_target -- so the backfill and a BEFORE INSERT trigger
share one definition rather than two copies of the same CASE drifting apart.
The trigger guards on NEW.actor_id IS NULL, which identifies a legacy insert
exactly because the repository always supplies one. That distinction matters
for target_id: a current writer sets it NULL deliberately for events with no
user target, and deriving there would name the actor as their own target.
Kept rather than scheduled for removal: it is a no-op on the path the
repository takes, and it keeps the column's contract true for any writer that
bypasses it.
Correctness
- /api/remote never recorded source.created, so URL, GitHub and connector
sources had a source.deleted with no matching creation. All three creation
paths now go through one _audit_source_created helper.
- The prompt-cache rate divided cached tokens by a whole bucket's prompt
tokens. A bucket is a day and mixes calls whose provider reports a cache
breakdown with calls whose provider does not, so filtering buckets in the
client could not separate them and the rate was understated by however much
traffic ran on a non-reporting provider. The denominator is now computed in
SQL over the reporting rows.
- The outcome pill matched values nothing writes. Guardrails emit triggered /
not_evaluated and the device feed emits dispatched; the map had blocked /
denied / allowed, so a guardrail that fired rendered neutral grey -- the one
signal the merged feed exists to surface. Fixtures were seeding the
fictional values, so the tests passed on it too.
- Stream duration_ms timed the consumer. stream_token_usage is a generator,
so start-to-exhaustion includes the agent loop's tool handling and the SSE
client's pace; a slow browser recorded ~30s for a sub-second call. It now
accumulates only the time spent inside next().
Safety
- Activity filters failed open: an unknown facet or unparseable timestamp was
dropped, and no filter means every row, so a typo widened an audit view and
on the export streamed the full history. Both are now a 400.
- The search term was interpolated into an ILIKE pattern, so "100%" matched
everything and "q1_report" matched more than it should. Escaped.
- 0034 set actor_id NOT NULL with no default. A previous-release process
inserting mid-rollout would raise, and in admin/routes.py that insert shares
the request transaction, so a role grant beside it would roll back too.
Noise and dead code
- The per-user panel is a security panel: data-plane events file under the
actor, so an active account's routine deletes pushed a denied login out of
the 20-row window. It now excludes them; the Activity tab shows everything.
- device_audit_log had no created_at-leading index, so the merged feed
sequentially scanned that branch every page (migration 0036).
- conversation.deleted_all no longer records when nothing was deleted, and
agent.updated no longer records an empty field list.
- Dropped by_model from /admin/usage (no consumer; an extra aggregate per page
load), the duplicate filter surface on AuthEventsRepository that nothing
called, and the unreachable FLOW_LABELS.schedule entry.
- Type hints on record_event's conn and the remaining unannotated helpers.
Covers what the previous commits changed: the actor_id / target_id split and
the system actors, the data-plane events, the merged Activity feed and its
export, and the new usage and per-user spend endpoints.
Also corrects "there is no UI for these events yet" in the OIDC login
auditing section, which is no longer true.
- check_usage treats a request through a keyless (draft) agent as agent
traffic, matching how its usage rows are bucketed and the headless rule
- dashboard edits carry the stored enabled flag instead of re-enabling the
policy; disabled policies are labelled in the Quotas tab and the editor
- quota 429s send x-should-retry: false so OpenAI SDK clients do not retry
a refusal that cannot succeed before the reset
- cached-input and cache-write rates for Anthropic, OpenRouter and Groq
gpt-oss-120b; refresh OpenRouter deepseek-v3.2 list prices
- UsageQuota reuses usagePercent; docs note that a user override needs an
existing user
- A tool continuation refused for usage now releases the resume claim it
took; before, retries got a 409 until the stale claim was reverted.
- Agent traffic is any row with an agent key or an agent id, so keyless
agents and workflow nodes count toward the agent bucket, not direct.
- The user quota modal discards responses for a previously opened user.
- The usage meter shows every limited bucket, not only 'all'.
- Restore the class separator on the analytics stat card that a formatter
run removed, and align the OpenRouter DeepSeek description with its rates.
How the instance default, team allowances and user overrides resolve
(including users in several teams), the quota window, who is charged for
agent traffic, how cost budgets price models and what happens to unpriced
ones, and the admin and user API.
Rename the unused *_cost_per_token capability fields to USD per 1M tokens,
add prompt-cache read/write rates, and ship list prices for the hosted
catalogs. The old per-token keys still load, scaled, with a warning.
docsgpt/pricing.py turns a call's token bins into a USD cost. Models with
no declared rate cost $0 unless QUOTA_UNPRICED_RATE_PER_MILLION is set.
POST /api/user/tokens/<id>/regenerate swaps the secret of an existing token
in place: name, scopes and restrictions stay, the old secret stops matching
at once, and the expiry is reset. The lifetime defaults to the one the token
was last issued with (clamped to today's policy) or to expires_in_days when
given. An expired token can be renewed this way; a revoked one cannot. It is
session only like the rest of token management, and writes a pat_regenerated
audit event. regenerated_at records the rotation.
A resource restriction was checked on ids in the request, not on what the
addressed row pulls in or belongs to. Closed:
- Workflow writes for tokens restricted on sources, tools or prompts (a graph
names those inside its nodes), and attaching a workflow to an agent unless
the token is restricted on workflows too.
- Chat for tools-restricted tokens (chat executes tools; rejected at token
creation as well), and agent-less chat for tokens restricted on prompts or
workflows.
- conversation_id on chat: it must belong to the agent being run, or to no
agent for agent-less chat. Otherwise the server continued, appended to, or
resumed pending tool calls of another agent's conversation.
- Schedules for tokens restricted on anything but agents; schedule-id routes
for every restricted token.
- Conversations and analytics for every restricted token, not only
agent-restricted ones.
Also: create, first publish and adopt return the agent API key masked to a
token without agents:keys; token ids must be canonical UUIDs (urn:uuid: gave
a 500); an expired token is reported as expired; token creation takes a
per-user advisory lock so the cap cannot be raced; admin revoke-sessions
writes a pat_revoked event per token. The UI drops a row whose revoke returns
404 and does not offer a tools restriction next to chat:run.
The ASGI message events route now accepts the same scopes as its Flask
sibling (conversations:read or chat:run) through a shared constant. A token
request that fails routing gets Flask's 404/405 instead of a 403. Creating a
token retires an expired token that still held the name. allowed_ids uses
is_pat instead of a bare literal. PAT_ENABLED is documented as the master
switch it is: turning it off stops every existing token from authenticating.
The docs explain that sources are matched by name (oldest wins) and point CI
flows at sources upload --replace.