Run webhooks with the owner's rules again

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.
This commit is contained in:
arc53-machine committed 2026-09-29 15:55:36 +01:00
1 parent 080b75c81e
commit 51f88144c5
14 files changed
+19 -19

No files matched your search

+3 -1
View File
@@ -49,10 +49,12 @@ Tools from OAuth MCP servers default to each member's own account. An admin can
### Agents used through an API key
An agent called with its API key (the website widget, the API) or fired by a webhook runs with its owner's connections, and nobody can approve an action there. It can read through those connections, but it can't take write actions on them unless the owner allows each one under **Access details > Actions API, widget and public-link users can take as you**. The same goes for tools that hold the owner's own credentials without a connection: an API tool action that sends a header or query value the owner saved (a key or token), or an MCP server the owner signed in to. Only the owner can change that list; team editors can't. The owner previewing their own agent in DocsGPT is not limited.
An agent called with its API key (the website widget, the API) runs with its owner's connections, and nobody can approve an action there. It can read through those connections, but it can't take write actions on them unless the owner allows each one under **Access details > Actions API, widget and public-link users can take as you**. The same goes for tools that hold the owner's own credentials without a connection: an API tool action that sends a header or query value the owner saved (a key or token), or an MCP server the owner signed in to. Only the owner can change that list; team editors can't. The owner previewing their own agent in DocsGPT is not limited.
The same list covers people who open the agent from its public link without being on a team it's shared with, and anything they schedule from that chat. They can't approve write actions on the owner's accounts or credentials, so those run only when the owner allows them there. On a tool where each member connects their own account, they approve writes on their own account as usual.
A webhook is the owner's own automation (its URL is a secret), so its runs are not limited by this list. Nobody can approve during a webhook run, so it still skips every action that needs approval.
### What answers show
Tool calls name the service that ran them: "Searched Notion", "Read from Google Drive", "Used Linear: create issue". Citations from a synced source read "From Google Drive". Answers never show which account was used.
+1 -1
View File
@@ -90,7 +90,7 @@ def run_agent_headless(
``trace_user_id`` owns the trace when the run belongs to someone other
than the agent's owner (a schedule a user set on a shared agent), so the
trace is visible wherever that user sees the run; it defaults to the owner.
``external_caller`` (a webhook, a schedule set through the API) and
``external_caller`` (a schedule set through the API) and
``public_link_caller`` (a schedule a public-link user set) mark a run for
someone who can't approve for the owner: writes on the owner's accounts
and credentials then run only when the agent's API write allowlist has
+1 -1
View File
@@ -1171,7 +1171,7 @@ class ToolExecutor:
entry = f"{tool_data.get('id') or tool_id}:{action_name}"
if entry in self.api_write_allowlist:
return None
route = "for API, widget or webhook callers" if self.external_caller else "from its public link"
route = "for API or widget callers" if self.external_caller else "from its public link"
target = (
f"the owner's {resolved.connector_name} account"
if resolved is not None and resolved.connector_name
+2 -2
View File
@@ -162,8 +162,8 @@ def owner_credential_writes(tool: dict) -> list[str]:
"""Names of the tool's write actions that run on its owner's credentials.
These are what someone who can't approve for the owner (an API-key or
widget caller, a public-link user, a webhook) may run only when the owner
allows them in the agent's API write allowlist.
widget caller, a public-link user) may run only when the owner allows
them in the agent's API write allowlist.
Args:
tool: A ``user_tools`` row.
-3
View File
@@ -2189,9 +2189,6 @@ def agent_webhook_worker(self, agent_id, payload):
input_data,
tool_allowlist=_webhook_tool_allowlist(agent_config),
endpoint="webhook",
# The payload comes from outside: nobody there can approve for
# the owner, so owner-account writes need the API allowlist.
external_caller=True,
request_id=getattr(getattr(self, "request", None), "id", None),
)
result = {
+2 -2
View File
@@ -35,8 +35,8 @@ type UserTool = {
/**
* The write actions on the owner's accounts and stored credentials that
* anyone reaching this agent through its API key, widget, a webhook or its
* public link may run. Nobody there can approve an action for the owner, so
* anyone reaching this agent through its API key, widget or its public link
* may run. Nobody there can approve an action for the owner, so
* the server refuses every other such write. The server names these writes
* per tool (`owner_credential_writes`).
*
+1 -1
View File
@@ -1890,7 +1890,7 @@
"resetKeyConfirm": "Möchten Sie den API-Schlüssel wirklich zurücksetzen? Der aktuelle Schlüssel funktioniert sofort nicht mehr und diese Aktion kann nicht rückgängig gemacht werden.",
"apiWrites": {
"title": "Aktionen, die API-, Widget- und Public-Link-Nutzer in deinem Namen ausführen können",
"description": "Über den API-Schlüssel, das Widget, einen Webhook oder den öffentlichen Link des Agenten kann niemand eine Aktion für dich bestätigen. Änderungen mit deinen verbundenen Konten und gespeicherten Zugangsdaten sind daher blockiert, außer du erlaubst sie hier.",
"description": "Über den API-Schlüssel, das Widget oder den öffentlichen Link des Agenten kann niemand eine Aktion für dich bestätigen. Änderungen mit deinen verbundenen Konten und gespeicherten Zugangsdaten sind daher blockiert, außer du erlaubst sie hier.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Speichern fehlgeschlagen. Versuche es erneut."
},
+1 -1
View File
@@ -1896,7 +1896,7 @@
"resetKeyConfirm": "Are you sure you want to reset the API key? The current key will stop working immediately and this action cannot be undone.",
"apiWrites": {
"title": "Actions API, widget and public-link users can take as you",
"description": "Nobody can approve an action for you through the API key, the widget, a webhook or the agent's public link, so changes made with your connected accounts and saved credentials are blocked unless you allow them here.",
"description": "Nobody can approve an action for you through the API key, the widget or the agent's public link, so changes made with your connected accounts and saved credentials are blocked unless you allow them here.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Could not save. Try again."
},
+1 -1
View File
@@ -1890,7 +1890,7 @@
"resetKeyConfirm": "¿Seguro que quieres restablecer la clave de API? La clave actual dejará de funcionar de inmediato y esta acción no se puede deshacer.",
"apiWrites": {
"title": "Acciones que los usuarios de la API, del widget y del enlace público pueden realizar en tu nombre",
"description": "Nadie puede aprobar una acción por ti a través de la clave de API, el widget, un webhook o el enlace público del agente, así que los cambios con tus cuentas conectadas y credenciales guardadas se bloquean salvo que los permitas aquí.",
"description": "Nadie puede aprobar una acción por ti a través de la clave de API, el widget o el enlace público del agente, así que los cambios con tus cuentas conectadas y credenciales guardadas se bloquean salvo que los permitas aquí.",
"item": "{{tool}}: {{action}}",
"saveFailed": "No se pudo guardar. Inténtalo de nuevo."
},
+1 -1
View File
@@ -1879,7 +1879,7 @@
"resetKeyConfirm": "APIキーをリセットしてもよろしいですか?現在のキーは直ちに無効になり、この操作は元に戻せません。",
"apiWrites": {
"title": "API、ウィジェット、公開リンクの利用者があなたとして実行できる操作",
"description": "API キー、ウィジェット、Webhook、エージェントの公開リンクでは誰もあなたの代わりに操作を承認できないため、接続済みアカウントや保存済みの認証情報を使う変更は、ここで許可しない限りブロックされます。",
"description": "API キー、ウィジェット、エージェントの公開リンクでは誰もあなたの代わりに操作を承認できないため、接続済みアカウントや保存済みの認証情報を使う変更は、ここで許可しない限りブロックされます。",
"item": "{{tool}}: {{action}}",
"saveFailed": "保存できませんでした。もう一度お試しください。"
},
+1 -1
View File
@@ -1974,7 +1974,7 @@
"resetKeyConfirm": "Вы уверены, что хотите сбросить API-ключ? Текущий ключ немедленно перестанет работать, и это действие нельзя отменить.",
"apiWrites": {
"title": "Действия, которые пользователи API, виджета и публичной ссылки могут выполнить от вашего имени",
"description": "Через API-ключ, виджет, вебхук или публичную ссылку агента никто не может подтвердить действие за вас, поэтому изменения с вашими подключёнными аккаунтами и сохранёнными учётными данными заблокированы, если вы не разрешите их здесь.",
"description": "Через API-ключ, виджет или публичную ссылку агента никто не может подтвердить действие за вас, поэтому изменения с вашими подключёнными аккаунтами и сохранёнными учётными данными заблокированы, если вы не разрешите их здесь.",
"item": "{{tool}}: {{action}}",
"saveFailed": "Не удалось сохранить. Попробуйте ещё раз."
},
+1 -1
View File
@@ -1879,7 +1879,7 @@
"resetKeyConfirm": "確定要重設 API 金鑰嗎?目前的金鑰將立即停止運作,此操作無法復原。",
"apiWrites": {
"title": "API、小工具和公開連結使用者能以你的身分執行的操作",
"description": "透過 API 金鑰、小工具、Webhook 或代理的公開連結,無人能替你核准操作,因此除非你在此允許,否則使用你的已連線帳號和已儲存憑證的變更都會被封鎖。",
"description": "透過 API 金鑰、小工具或代理的公開連結,無人能替你核准操作,因此除非你在此允許,否則使用你的已連線帳號和已儲存憑證的變更都會被封鎖。",
"item": "{{tool}}:{{action}}",
"saveFailed": "無法儲存。請再試一次。"
},
+1 -1
View File
@@ -1879,7 +1879,7 @@
"resetKeyConfirm": "确定要重置 API 密钥吗?当前密钥将立即停止工作,此操作无法撤销。",
"apiWrites": {
"title": "API、小组件和公开链接用户能以你的身份执行的操作",
"description": "通过 API 密钥、小组件、Webhook 或智能体的公开链接,无人能替你批准操作,因此除非你在此允许,否则使用你的已连接账号和已保存凭据的更改都会被阻止。",
"description": "通过 API 密钥、小组件或智能体的公开链接,无人能替你批准操作,因此除非你在此允许,否则使用你的已连接账号和已保存凭据的更改都会被阻止。",
"item": "{{tool}}:{{action}}",
"saveFailed": "无法保存。请重试。"
},
+3 -2
View File
@@ -75,8 +75,9 @@ class TestAgentWebhookWorker:
assert captured["input"] == '{"event": "ping"}'
# Webhook caller should pass endpoint='webhook'.
assert captured["kwargs"].get("endpoint") == "webhook"
# The payload comes from outside: nobody there can approve for the owner.
assert captured["kwargs"].get("external_caller") is True
# The owner set the webhook up (its URL is a secret): the run keeps the
# owner's rules, and headless runs still refuse approval-gated tools.
assert not captured["kwargs"].get("external_caller")
def test_missing_agent_raises(
self, pg_conn, patch_worker_db, task_self, monkeypatch