feat(agents): warn in the editor when a first grant turns off connector features

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
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 19:38:55 +00:00
1 parent 6cbee5181c
commit 9b40c24a30
18 files changed
+241 -26

No files matched your search

+27 -3
View File
@@ -35,10 +35,34 @@ test.describe('Agent form', () => {
await expect(page).toHaveURL(/\/app\/agents$/)
expect(s.calls.find(c => c.method === 'POST').body).toEqual({
name: 'Reviewer', description: 'Checks a change', instructions: 'Answer in French.', model: '',
mcp_servers: ['connector:m1'], addons: [], collections: ['handbook'], skills: ['citations'], builtins_off: [], builtins_on: [], network: 'off',
mcp_servers: ['connector:m1'], addons: [], collections: ['handbook'], skills: ['citations'], builtins_off: [], builtins_on: [], builtins: [], network: 'off',
})
})
test('warns when the first permission turns off the messaging defaults, and adds the missing ones in one click', async ({ page }) => {
const s = await installAgents(page)
await page.goto('/app/agents/a2/edit')
await fold(page, 'Grants').locator('.ag-fold__button').click()
await expect(page.getByTestId('agent-grants-notice')).toHaveCount(0)
await page.getByTestId('agent-grant').filter({ hasText: 'Remember things' }).getByRole('checkbox').check()
const notice = page.getByTestId('agent-grants-notice')
await expect(notice).toContainText('triggers.propose, triggers.run, talkback.channels, files.upload')
await page.getByTestId('agent-grants-add-missing').click()
await expect(notice).toHaveCount(0)
await page.getByRole('button', { name: 'Save changes' }).first().click()
await expect(page).toHaveURL(/\/app\/agents$/)
const put = s.calls.find(c => c.method === 'PUT')
expect(put.body.builtins.sort()).toEqual(['files.upload', 'memory.write', 'talkback.channels', 'triggers.propose', 'triggers.run'])
})
test('shows no warning for an agent that already had permissions', async ({ page }) => {
await installAgents(page, { agents: [{ ...AGENTS[1], builtins: ['memory.write'] }] })
await page.goto('/app/agents/a2/edit')
await fold(page, 'Grants').locator('.ag-fold__button').click()
await page.getByTestId('agent-grant').filter({ hasText: 'Propose triggers' }).getByRole('checkbox').check()
await expect(page.getByTestId('agent-grants-notice')).toHaveCount(0)
})
test('sets the network of the commands of unattended runs', async ({ page }) => {
const s = await installAgents(page)
await page.goto('/app/agents/new')
@@ -238,7 +262,7 @@ test.describe('Agent folder', () => {
test('read only shows the lock chip, and the mode switch sends write', async ({ page }) => {
const s = await installAgents(page, { files: HOME_FOLDERS, agents: withFolder(ACME_READ) })
await page.goto('/app/agents/a2')
await page.goto('/app/agents/a2/edit')
const chip = page.getByTestId('agent-facts').getByTestId('agent-folder-chip')
await expect(chip).toContainText('Acme, read only')
await expect(chip.locator('[data-icon="lock"]')).toHaveCount(1)
@@ -279,7 +303,7 @@ test.describe('Agent folder', () => {
const chip = page.locator('[data-agent="code-reviewer"]').getByTestId('agent-folder-chip')
await expect(chip).toHaveText('Folder missing')
await page.goto('/app/agents/a2')
await page.goto('/app/agents/a2/edit')
await expect(page.getByTestId('agent-folder-warning')).toContainText('This folder is gone. Choose another folder to use this agent.')
await page.getByTestId('agent-facts').getByTestId('agent-folder-chip').click()
await expect(page).toHaveURL(/\/app\/agents\/a2\/edit$/)
+10
View File
@@ -17,6 +17,15 @@ export const AGENTS = [
},
]
export const GRANTS = [
{ name: 'memory.write', title: 'Remember things', description: 'The agent can propose a note in your memory.', sensitive: false },
{ name: 'triggers.propose', title: 'Propose triggers', description: 'The agent can propose a task that runs by itself.', sensitive: false },
{ name: 'triggers.run', title: 'Run triggers', description: 'The triggers of this agent may start from a messaging app.', sensitive: false },
{ name: 'talkback.channels', title: 'Reply on known channels', description: 'The agent can send a message to a channel it already talks in.', sensitive: false },
{ name: 'files.upload', title: 'Receive files', description: 'Senders can attach files for the agent to read.', sensitive: false },
{ name: 'profile.write', title: 'Change its own rules', description: 'The agent can propose an edit to its profile.', sensitive: true },
]
const iso = (minutesAgo) => new Date(Date.now() - minutesAgo * 60_000).toISOString()
export const CHATS = [
@@ -168,6 +177,7 @@ export async function installAgents(page, {
// Agent files and run history, as the server answers them: an export is the
// definition without id or owner, an import always makes a new agent, and
// the runs are the chats that ran with the agent.
await page.route('**/api/chats/agents/builtin-grants', route => route.fulfill({ json: { grants: GRANTS } }))
await page.route(/\/api\/chats\/agents\/import$/, route => {
const req = route.request()
rec(req)
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "Dateien",
"Tools": "Werkzeuge"
"Tools": "Werkzeuge",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Wähle, was ein Chat mit diesem Agenten nutzen kann. Das sind dieselben Schalter wie im Werkzeuge-Menü eines Chats. Ein ausgeschaltetes Werkzeug wird dem Modell nie angeboten, und ein Chat mit diesem Agenten kann es nicht einschalten.",
"toolsHeading": "Werkzeuge dieses Agenten",
"skillsLater": "Ein Chat mit diesem Agenten liest nur die eingeschalteten Skills.",
"skillsHeading": "Skills dieses Agenten"
"skillsHeading": "Skills dieses Agenten",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -190,8 +190,16 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "Files",
"Tools": "Tools"
"Tools": "Tools",
"Grants": "Permissions"
},
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions",
"name": "Name",
"description": "Description",
"descriptionHelp": "One line that says what the agent is for. It shows in the list.",
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "Archivos",
"Tools": "Herramientas"
"Tools": "Herramientas",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Elige qué puede usar un chat con este agente. Son los mismos interruptores que el menú Herramientas de un chat. Una herramienta apagada nunca se ofrece al modelo, y un chat con este agente no puede encenderla.",
"toolsHeading": "Herramientas de este agente",
"skillsLater": "Un chat con este agente solo lee las skills activas.",
"skillsHeading": "Skills de este agente"
"skillsHeading": "Skills de este agente",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "Fichiers",
"Tools": "Outils"
"Tools": "Outils",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Choisissez ce qu'un chat avec cet agent peut utiliser. Ce sont les mêmes interrupteurs que le menu Outils d'un chat. Un outil désactivé n'est jamais proposé au modèle, et un chat avec cet agent ne peut pas l'activer.",
"toolsHeading": "Outils de cet agent",
"skillsLater": "Un chat avec cet agent ne lit que les skills activées.",
"skillsHeading": "Skills de cet agent"
"skillsHeading": "Skills de cet agent",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "File",
"Tools": "Alat"
"Tools": "Alat",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Pilih apa yang dapat dipakai chat dengan agen ini. Sakelarnya sama dengan menu Alat pada chat. Alat yang mati tidak pernah ditawarkan kepada model, dan chat dengan agen ini tidak dapat menyalakannya.",
"toolsHeading": "Alat agen ini",
"skillsLater": "Chat dengan agen ini hanya membaca skill yang aktif.",
"skillsHeading": "Skill agen ini"
"skillsHeading": "Skill agen ini",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "File",
"Tools": "Strumenti"
"Tools": "Strumenti",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Scegli cosa può usare una chat con questo agente. Sono gli stessi interruttori del menu Strumenti di una chat. Uno strumento spento non viene mai offerto al modello, e una chat con questo agente non può accenderlo.",
"toolsHeading": "Strumenti di questo agente",
"skillsLater": "Una chat con questo agente legge solo le skill attive.",
"skillsHeading": "Skill di questo agente"
"skillsHeading": "Skill di questo agente",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "파일",
"Tools": "도구"
"Tools": "도구",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "이 에이전트와의 채팅이 사용할 수 있는 것을 고르세요. 채팅의 도구 메뉴와 같은 스위치입니다. 꺼진 도구는 모델에 제공되지 않으며, 이 에이전트와의 채팅에서 켤 수 없습니다.",
"toolsHeading": "이 에이전트의 도구",
"skillsLater": "이 에이전트와의 채팅은 켜진 스킬만 읽습니다.",
"skillsHeading": "이 에이전트의 스킬"
"skillsHeading": "이 에이전트의 스킬",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "Arquivos",
"Tools": "Ferramentas"
"Tools": "Ferramentas",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "Escolha o que um chat com este agente pode usar. São os mesmos interruptores do menu Ferramentas de um chat. Uma ferramenta desligada nunca é oferecida ao modelo, e um chat com este agente não pode ligá-la.",
"toolsHeading": "Ferramentas deste agente",
"skillsLater": "Um chat com este agente lê apenas as skills ligadas.",
"skillsHeading": "Skills deste agente"
"skillsHeading": "Skills deste agente",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
@@ -81,7 +81,8 @@
"MCP": "MCP servers",
"export": "Export",
"Files": "文件",
"Tools": "工具"
"Tools": "工具",
"Grants": "Permissions"
},
"name": "Name",
"description": "Description",
@@ -139,7 +140,14 @@
"toolsHelp": "选择使用此智能体的聊天可以用什么。这些开关与聊天的工具菜单相同。关闭的工具不会提供给模型,使用此智能体的聊天也无法将其打开。",
"toolsHeading": "此智能体的工具",
"skillsLater": "使用此智能体的聊天只会读取已开启的技能。",
"skillsHeading": "此智能体的技能"
"skillsHeading": "此智能体的技能",
"grantsHelp": "What the agent may do beyond its tools. With no permission ticked, the agent keeps the defaults it had before permissions existed. Once you tick any, only the ticked ones work.",
"grantsDefault": "Defaults",
"countGrants": "{{count}} granted",
"grantSensitive": "needs care",
"grantsNoticeTitle": "Some messaging features will turn off.",
"grantsNotice": "This agent had no permission list, so it kept these on its messaging connections: {{grants}}. They stop working until you tick them here.",
"grantsAddMissing": "Add the missing permissions"
},
"run": {
"workingLine": "Working"
+46 -1
View File
@@ -7,7 +7,7 @@ import { agentCollectionsApi, chatAgentsApi, modelMCPServersApi } from '../utils
import { useAgentTools } from '../hooks/useAgentTools'
import { useToolGroups } from '../hooks/useToolGroups'
import { AGENT_TEMPLATES, diffConfig, displayValue, maskSecrets, summarise } from '../utils/agentConfigTools'
import { agentPayload, effectiveModes, emptyAgent, folderRows, folderState, foldersUnchosen, MAX_FOLDERS } from '../utils/chatAgents'
import { agentPayload, effectiveModes, emptyAgent, folderRows, folderState, foldersUnchosen, MAX_FOLDERS, missingConnectorGrants, setGrant } from '../utils/chatAgents'
import { downloadAgentFile } from '../utils/agentFiles'
import { joinAgentMCP, splitAgentMCP } from '../utils/chatMCP'
import './agents.css'
@@ -40,6 +40,7 @@ const SECTIONS = [
{ id: 'MemorySettings', icon: 'database' },
{ id: 'Skills', icon: 'sparkles' },
{ id: 'Tools', icon: 'plug' },
{ id: 'Grants', icon: 'shield' },
{ id: 'MCP', icon: 'server' },
]
@@ -79,6 +80,7 @@ export default function AgentCreate() {
const initialFormRef = useRef(null)
const [collectionNames, setCollectionNames] = useState([])
const [modelServers, setModelServers] = useState([])
const [grantList, setGrantList] = useState([])
// Suggestions are best effort: a failure leaves the field free text.
useEffect(() => {
@@ -87,6 +89,13 @@ export default function AgentCreate() {
.catch(() => {})
}, [])
// The grants the editor can list: best effort, an empty list hides the checklist.
useEffect(() => {
chatAgentsApi.builtinGrants()
.then(r => setGrantList(Array.isArray(r?.grants) ? r.grants : []))
.catch(() => {})
}, [])
// The servers of the fixed model are suggestions; with no fixed model the field is free text.
const fixedModel = form.model || ''
useEffect(() => {
@@ -142,6 +151,10 @@ export default function AgentCreate() {
// A folder row was added or changed and has no folder chosen.
const folderUnchosen = foldersUnchosen(initialFolders, form.folders)
// Adding the first grant to an agent that had none turns off what it did
// by default on its messaging connections. Say so, and offer the fix.
const missingGrants = isEdit ? missingConnectorGrants(savedConfig?.builtins, form.builtins) : []
const handleSubmit = async (e) => {
e?.preventDefault?.()
if (!payload.name) {
@@ -210,6 +223,12 @@ export default function AgentCreate() {
summary = total ? t('create.toolsSummary', { on, total }) : t('create.noneYet')
state = on ? 'ready' : 'empty'
}
else if (s.id === 'Grants') {
keys = ['builtins']
const n = (form.builtins || []).length
summary = n ? t('create.countGrants', { count: n }) : t('create.grantsDefault')
state = missingGrants.length ? 'needs' : 'ready'
}
else if (s.id === 'Files') { keys = ['folders']; ({ summary, state } = folderFold()) }
else if (s.id === 'MCP') { keys = ['mcp_servers']; summary = countOf(form.mcp_servers.length, 'create.countServers'); state = form.mcp_servers.length ? 'ready' : 'empty' }
else {
@@ -343,6 +362,32 @@ export default function AgentCreate() {
</div>
</>
)
case 'Grants':
return (
<>
<p className="ag-note">{t('create.grantsHelp')}</p>
{missingGrants.length > 0 && (
<Callout
tone="warn" testId="agent-grants-notice"
title={t('create.grantsNoticeTitle')}
actions={<Button size="sm" onClick={() => updateField('builtins', missingGrants.reduce((l, g) => setGrant(l, g, true), form.builtins || []))} data-testid="agent-grants-add-missing">{t('create.grantsAddMissing')}</Button>}
>
{t('create.grantsNotice', { grants: missingGrants.join(', ') })}
</Callout>
)}
<div className="ag-grants" data-testid="agent-grants">
{grantList.map(g => (
<label key={g.name} className="ag-grants__row" data-testid="agent-grant">
<input type="checkbox" checked={(form.builtins || []).includes(g.name)} onChange={(e) => updateField('builtins', setGrant(form.builtins, g.name, e.target.checked))} />
<span className="ag-grants__text">
<span className="ag-grants__title">{g.title}{g.sensitive && <span className="ag-grants__tag">{t('create.grantSensitive')}</span>}</span>
<span className="ag-note">{g.description}</span>
</span>
</label>
))}
</div>
</>
)
case 'Files': {
const rows = form.folders || []
const effective = effectiveModes(rows)
+8
View File
@@ -354,3 +354,11 @@ a.ag-run-row:hover { background: color-mix(in srgb, var(--dk-hover) 45%, transpa
margin-bottom: var(--sp-4);
}
.ag-switches .swl-list { overflow-y: visible; }
/* The grants checklist of the agent editor. */
.ag-grants { display: flex; flex-direction: column; gap: var(--sp-2); margin-top: var(--sp-3); }
.ag-grants__row { display: flex; align-items: flex-start; gap: var(--sp-2); cursor: pointer; }
.ag-grants__row input { margin-top: 3px; }
.ag-grants__text { display: flex; flex-direction: column; min-width: 0; }
.ag-grants__title { font-weight: 500; }
.ag-grants__tag { margin-left: var(--sp-2); font-size: var(--fs-small); color: var(--dk-muted); }
+25 -2
View File
@@ -2,10 +2,33 @@
// builtins_off and builtins_on are the switches of the built-in tool groups
// (see utils/agentTools.js); they are lists like the grants.
export const LIST_KEYS = ['mcp_servers', 'addons', 'collections', 'skills', 'builtins_off', 'builtins_on']
export const LIST_KEYS = ['mcp_servers', 'addons', 'collections', 'skills', 'builtins_off', 'builtins_on', 'builtins']
// The grants that an agent without a grant list holds for its messaging
// connections (not sensitive, so implied). As soon as the list names any
// grant, only the named ones work: these four are then off until added. The
// server rule is core/services/studio/connectorgrants.go (grantGated).
export const CONNECTOR_GRANTS = ['triggers.propose', 'triggers.run', 'talkback.channels', 'files.upload']
// The notice for the grants list: set when the saved agent had no list, the
// form now has one, and some connector grants are missing from it. Returns
// the missing names, or an empty array when nothing would turn off.
export function missingConnectorGrants(saved, current) {
const had = (saved || []).length > 0
const now = current || []
if (had || now.length === 0) return []
return CONNECTOR_GRANTS.filter(g => !now.includes(g))
}
// The grants list with a name switched on or off.
export function setGrant(list, name, on) {
const rest = (list || []).filter(x => x !== name)
return on ? [...rest, name] : rest
}
export function emptyAgent() {
return { id: '', name: '', description: '', instructions: '', model: '', mcp_servers: [], addons: [], collections: [], skills: [], builtins_off: [], builtins_on: [], network: 'off', folders: [] }
return { id: '', name: '', description: '', instructions: '', model: '', mcp_servers: [], addons: [], collections: [], skills: [], builtins_off: [], builtins_on: [], builtins: [], network: 'off', folders: [] }
}
// Adds a name to a list: trimmed, not empty, once.
@@ -1,6 +1,6 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { addEntry, agentFolderMarks, agentPayload, emptyAgent, effectiveModes, folderName, folderPickable, folderRows, folderState, foldersPayload, foldersUnchosen, MAX_FOLDERS, grantSummary, isFolderMissingError, removeEntry } from '../../src/utils/chatAgents.js'
import { addEntry, agentFolderMarks, agentPayload, emptyAgent, effectiveModes, folderName, folderPickable, folderRows, folderState, foldersPayload, foldersUnchosen, MAX_FOLDERS, grantSummary, isFolderMissingError, removeEntry, missingConnectorGrants, setGrant, CONNECTOR_GRANTS } from '../../src/utils/chatAgents.js'
test('chat agents: an entry is trimmed and added once', () => {
assert.deepEqual(addEntry([], ' office '), ['office'])
@@ -113,3 +113,22 @@ test('chat agents: a 409 agent_folder_missing is told apart from other errors',
assert.equal(isFolderMissingError(new Error('x')), false)
assert.equal(isFolderMissingError(null), false)
})
test('chat agents: the first grant on an agent without a list names the connector grants that turn off', () => {
assert.deepEqual(missingConnectorGrants([], ['memory.write']), CONNECTOR_GRANTS)
assert.deepEqual(missingConnectorGrants(undefined, ['memory.write', 'triggers.run']), ['triggers.propose', 'talkback.channels', 'files.upload'])
assert.deepEqual(missingConnectorGrants([], CONNECTOR_GRANTS), [])
})
test('chat agents: no notice when the agent had a list, or the form still has none', () => {
assert.deepEqual(missingConnectorGrants(['memory.write'], ['memory.write', 'skills.write']), [])
assert.deepEqual(missingConnectorGrants([], []), [])
assert.deepEqual(missingConnectorGrants([], undefined), [])
})
test('chat agents: a grant is switched on once and off again', () => {
assert.deepEqual(setGrant([], 'memory.write', true), ['memory.write'])
assert.deepEqual(setGrant(['memory.write'], 'memory.write', true), ['memory.write'])
assert.deepEqual(setGrant(['memory.write', 'files.upload'], 'memory.write', false), ['files.upload'])
assert.deepEqual(agentPayload({ ...emptyAgent(), name: 'a', builtins: ['memory.write', ' memory.write'] }).builtins, ['memory.write'])
})
@@ -180,3 +180,17 @@ var _ = Describe("a tool gated by a capability", func() {
Expect(audited).To(Equal([]string{"memory.write"}))
})
})
var _ = Describe("the files.upload grant", func() {
// The connector gate enforces files.upload on the files a sender attaches to
// a message (the sender's files going to the agent). The text of the editor
// must say that, not the opposite direction.
It("says that senders' files reach the agent, and not that the agent sends files", func() {
g, ok := builtins.LookupGrant(builtins.GrantFilesUpload)
Expect(ok).To(BeTrue())
Expect(g.Title).To(Equal("Receive files"))
Expect(g.Description).To(ContainSubstring("attach files for the agent to read"))
Expect(g.Title + g.Description).NotTo(ContainSubstring("from your folders"))
Expect(g.Sensitive).To(BeFalse())
})
})
+1 -1
View File
@@ -60,7 +60,7 @@ var grants = []Grant{
{GrantTriggersRun, "Run triggers", "The triggers of this agent may start from a messaging app.", false},
{GrantTalkbackChannels, "Reply on known channels", "The agent can send a message to a channel it already talks in.", false},
{GrantTalkbackNewChan, "Start new channels", "The agent can start a conversation on a channel it has not used before.", true},
{GrantFilesUpload, "Send files", "The agent can send a file from your folders to a channel.", false},
{GrantFilesUpload, "Receive files", "In a chat that a messaging app starts, a sender can attach files for the agent to read. It does not let the agent send files out.", false},
{GrantActFiles, "Write files on its own", "A run that nobody watches may write files inside the agent's folders.", true},
{GrantActAddons, "Use add-on actions on its own", "A run that nobody watches may call the actions of the add-ons it has.", true},
{GrantActSend, "Send messages on its own", "A run that nobody watches may send messages beyond its delivery target.", true},
+1 -1
View File
@@ -269,7 +269,7 @@ In a chat that a messaging connection started, the grants also decide which tool
the model gets. A tool exists when the sender holds the capability on the
connection, the agent holds the grant, and the chat did not switch the tool off.
Four grants work this way: `triggers.propose`, `triggers.run`, `talkback.channels`
and `files.upload` (the sender's files going to the agent), plus the sensitive
and `files.upload` (a sender's files going to the agent; it does not let the agent send files out), plus the sensitive
`talkback.new_channel`. An agent that has no `builtins` list at all keeps the
non-sensitive ones, as before. A sensitive grant is never implied: it needs to be
on the list. The moment you grant any built-in, the list is the whole answer, so