diff --git a/core/http/react-ui/e2e/chats-proposal-card.spec.js b/core/http/react-ui/e2e/chats-proposal-card.spec.js index 67f61ec634..68d83ecaef 100644 --- a/core/http/react-ui/e2e/chats-proposal-card.spec.js +++ b/core/http/react-ui/e2e/chats-proposal-card.spec.js @@ -21,11 +21,11 @@ const HISTORY = [ ] // The server, in memory: the state of the proposal and what each call does. -async function serve(page, { messages = HISTORY, undoRefusal = '', confirmFails = false } = {}) { +async function serve(page, { messages = HISTORY, undoRefusal = '', confirmFails = false, extra = {} } = {}) { const s = { state: 'pending', canUndo: true, blocked: '', calls: [] } const view = () => ({ state: s.state, - trigger: trigger(s.state === 'accepted' ? { enabled: true, next_run: '2026-11-02T08:00:00Z' } : {}), + trigger: trigger({ ...extra, ...(s.state === 'accepted' ? { enabled: true, next_run: '2026-11-02T08:00:00Z' } : {}) }), answered_at: s.state === 'pending' ? undefined : AT, can_undo: s.state === 'accepted' && s.canUndo, undo_blocked: s.blocked || undefined, @@ -36,6 +36,10 @@ async function serve(page, { messages = HISTORY, undoRefusal = '', confirmFails await page.route('**/api/chats/c1', route => route.fulfill({ json: detail })) await page.route(/\/api\/chats(\?.*)?$/, route => route.fulfill({ json: { conversations: [{ ...detail, pending: undefined }], persistent: true, files: true } })) await page.route('**/api/chats/agents*', route => route.fulfill({ json: { agents: [] } })) + await page.route('**/api/chats/agents/builtin-grants', route => route.fulfill({ json: { grants: [ + { name: 'act.files', title: 'Write files on its own', description: 'A run that nobody watches may write files inside the agent\'s folders.', sensitive: true }, + { name: 'act.spend', title: 'Use paid or external tools on its own', description: 'A run that nobody watches may use paid or external tools and the network.', sensitive: true }, + ] } })) await page.route('**/api/chats/triggers/preview*', route => route.fulfill({ json: { times: ['2026-11-02T08:00:00Z'], summary: 'Every Monday at 09:00' } })) await page.route(/\/api\/chats\/triggers\/proposals\/p1(\/[a-z]+)?$/, (route) => { const req = route.request() @@ -100,6 +104,25 @@ for (const [name, size] of [['desktop', { width: 1440, height: 900 }], ['phone', await page.screenshot({ path: `test-results/proposal-${name}-pending.png` }) }) + test('the card says a run only reads when the trigger holds no act, and why', async ({ page }) => { + await open(page, { extra: { acts: [], acts_held: [], read_only: true, read_only_why: 'no_acts' } }) + const sum = card(page).getByTestId('trigger-acts-summary') + await expect(sum).toHaveAttribute('data-read-only', 'true') + await expect(sum).toContainText('Read only') + await expect(card(page).getByTestId('trigger-acts-why')).toContainText('only read and report') + }) + + test('the card lists the acts the trigger would run with, sensitive ones marked, before it is created', async ({ page }) => { + const s = await open(page, { extra: { acts: ['act.files', 'act.spend'], acts_held: ['act.files'], read_only: false } }) + const sum = card(page).getByTestId('trigger-acts-summary') + await expect(sum.locator('[data-act="act.files"]')).toContainText('Write files on its own') + await expect(sum.locator('[data-act="act.files"]')).toContainText('needs care') + await expect(sum.locator('[data-act="act.spend"]')).toHaveCount(0) + await expect(card(page).getByTestId('trigger-acts-unused')).toContainText('Use paid or external tools on its own') + expect(s.calls).not.toContain('POST confirm') + await page.screenshot({ path: `test-results/proposal-${name}-acts.png` }) + }) + test('accepting leaves a receipt in place, undo takes it back, and the state survives a reload', async ({ page }) => { const s = await open(page) await card(page).getByRole('button', { name: 'Create trigger' }).click() diff --git a/core/http/react-ui/public/locales/de/agents.json b/core/http/react-ui/public/locales/de/agents.json index 36548bbdfd..74ff6bf423 100644 --- a/core/http/react-ui/public/locales/de/agents.json +++ b/core/http/react-ui/public/locales/de/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Berechtigungen" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Berechtigungen" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Darf {{count}} Art von Aktion nutzen", + "acts_other": "Darf {{count}} Arten von Aktionen nutzen" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/de/chats.json b/core/http/react-ui/public/locales/de/chats.json index 012bcf24c6..6a2c2c9bf1 100644 --- a/core/http/react-ui/public/locales/de/chats.json +++ b/core/http/react-ui/public/locales/de/chats.json @@ -56,7 +56,8 @@ "costUnknown_other": "Die Tokens eines Laufs sind nach dem ersten Lauf bekannt. Höchstens {{count}} Läufe pro Tag.", "remoteNote": "{{model}} ist ein entfernter Dienst: Was ein Lauf liest, verlässt diesen Server.", "stopWidget": "Pausiere ihn im Widget oder lösche ihn unter Trigger", - "stop": "Schalte ihn aus oder lösche ihn unter Trigger" + "stop": "Schalte ihn aus oder lösche ihn unter Trigger", + "actsLabel": "Berechtigungen" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "{{count}} Dateien löschen", "cleared_one": "{{count}} Datei in den Papierkorb verschoben.", "cleared_other": "{{count}} Dateien in den Papierkorb verschoben." + }, + "triggerActs": { + "help": "Ein Lauf startet von selbst, also bestätigt niemand einen Schritt. Er kann lesen und berichten. Hake an, was er zusätzlich tun darf. Er bekommt nie mehr, als sein Agent hat.", + "noAgent": "Es ist kein Agent gewählt, daher begrenzt nur das Angehakte den Lauf.", + "readOnly": "Nur lesen", + "canAct": "Darf selbstständig handeln", + "canActText_one": "Ein Lauf darf {{count}} Art von Aktion ohne Rückfrage nutzen.", + "canActText_other": "Ein Lauf darf {{count}} Arten von Aktionen ohne Rückfrage nutzen.", + "sensitive": "mit Vorsicht", + "addonRow": "Ein Lauf darf die Aktionen des Add-ons {{id}} nutzen.", + "blocked": "Dein Agent hat dies nicht, daher lässt es sich nicht anhaken. Füge es zuerst dem Agenten hinzu.", + "unused": "Dein Agent hat dies nicht, daher nutzt ein Lauf es nicht.", + "unusedList": "Aufgeführt, aber nicht genutzt, weil der Agent es nicht hat: {{names}}.", + "writesNote": "Sein Plan nennt die Datei {{file}}, daher darf ein Lauf auch Dateien schreiben.", + "listCan_one": "Kann handeln ({{count}})", + "listCan_other": "Kann handeln ({{count}})", + "why": { + "no_acts": "Keine Berechtigung zum Handeln gesetzt, daher lesen und berichten seine Läufe nur.", + "agent": "Sein Agent hat die verlangten Berechtigungen nicht, daher lesen und berichten seine Läufe nur.", + "declared": "Er wurde so eingerichtet, dass er nur liest und berichtet. Nichts, was hier angehakt wird, ändert das.", + "binding": "Eine Verbindung im Nur-Lesen-Modus hat ihn erstellt. Nichts, was hier angehakt wird, ändert das." + } } } diff --git a/core/http/react-ui/public/locales/en/agents.json b/core/http/react-ui/public/locales/en/agents.json index bf13f3cf8e..0e50b7b8a2 100644 --- a/core/http/react-ui/public/locales/en/agents.json +++ b/core/http/react-ui/public/locales/en/agents.json @@ -419,7 +419,8 @@ "runMeta": "took {{time}}", "agent": "Agent", "cap": "Most runs a day", - "watches": "Watches the folder {{folder}}" + "watches": "Watches the folder {{folder}}", + "acts": "Permissions" }, "edit": { "titleNew": "New trigger", @@ -444,7 +445,8 @@ "schedule": "When", "media": "Media sources", "webhooks": "Webhooks", - "limits": "Limits" + "limits": "Limits", + "acts": "Permissions" }, "summary": { "basics": "{{name}}, {{model}}", @@ -453,7 +455,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "May use {{count}} kind of action", + "acts_other": "May use {{count}} kinds of action" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/en/chats.json b/core/http/react-ui/public/locales/en/chats.json index c50966a2e8..b6a2ad9156 100644 --- a/core/http/react-ui/public/locales/en/chats.json +++ b/core/http/react-ui/public/locales/en/chats.json @@ -723,7 +723,8 @@ "costUnknown_other": "The tokens of a run are known after the first run. At most {{count}} runs a day.", "remoteNote": "{{model}} is a remote service: what a run reads leaves this server.", "stopWidget": "Pause it from the widget, or delete it in Triggers", - "stop": "Switch it off or delete it in Triggers" + "stop": "Switch it off or delete it in Triggers", + "actsLabel": "Permissions" } }, "subAgents": { @@ -1052,5 +1053,27 @@ "clearYes_other": "Delete {{count}} files", "cleared_one": "Moved {{count}} file to the Trash.", "cleared_other": "Moved {{count}} files to the Trash." + }, + "triggerActs": { + "help": "A run starts by itself, so nobody is there to approve a step. It can read and report. Tick what it may also do. It never gets more than its agent holds.", + "noAgent": "No agent is chosen, so only what you tick limits it.", + "readOnly": "Read only", + "canAct": "May act on its own", + "canActText_one": "A run may use {{count}} kind of action without asking.", + "canActText_other": "A run may use {{count}} kinds of action without asking.", + "sensitive": "needs care", + "addonRow": "A run may use the actions of the add-on {{id}}.", + "blocked": "Your agent does not hold this, so it cannot be ticked. Add it to the agent first.", + "unused": "Your agent does not hold this, so a run does not use it.", + "unusedList": "Listed, but not used because the agent does not hold it: {{names}}.", + "writesNote": "Its plan names the file {{file}}, so a run may also write files.", + "listCan_one": "Can act ({{count}})", + "listCan_other": "Can act ({{count}})", + "why": { + "no_acts": "No permission to act is set, so its runs only read and report.", + "agent": "Its agent does not hold the permissions it asks for, so its runs only read and report.", + "declared": "It was set up to read and report only. Nothing ticked here changes that.", + "binding": "A connection in read only mode made it. Nothing ticked here changes that." + } } } diff --git a/core/http/react-ui/public/locales/es/agents.json b/core/http/react-ui/public/locales/es/agents.json index 2cbe6bd5e7..82bde6ef5c 100644 --- a/core/http/react-ui/public/locales/es/agents.json +++ b/core/http/react-ui/public/locales/es/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Permisos" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Permisos" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Puede usar {{count}} tipo de acción", + "acts_other": "Puede usar {{count}} tipos de acción" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/es/chats.json b/core/http/react-ui/public/locales/es/chats.json index ae6d5f4da8..cc98466293 100644 --- a/core/http/react-ui/public/locales/es/chats.json +++ b/core/http/react-ui/public/locales/es/chats.json @@ -56,7 +56,8 @@ "costUnknown_other": "Los tokens de una ejecución se conocen tras la primera. Como máximo {{count}} ejecuciones al día.", "remoteNote": "{{model}} es un servicio remoto: lo que lee una ejecución sale de este servidor.", "stopWidget": "Páusalo desde el widget o elimínalo en Disparadores", - "stop": "Apágalo o elimínalo en Disparadores" + "stop": "Apágalo o elimínalo en Disparadores", + "actsLabel": "Permisos" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "Eliminar {{count}} archivos", "cleared_one": "{{count}} archivo movido a la Papelera.", "cleared_other": "{{count}} archivos movidos a la Papelera." + }, + "triggerActs": { + "help": "Una ejecución empieza sola, así que nadie aprueba un paso. Puede leer e informar. Marca lo que además puede hacer. Nunca recibe más de lo que tiene su agente.", + "noAgent": "No hay agente elegido, así que solo lo que marques la limita.", + "readOnly": "Solo lectura", + "canAct": "Puede actuar por sí misma", + "canActText_one": "Una ejecución puede usar {{count}} tipo de acción sin preguntar.", + "canActText_other": "Una ejecución puede usar {{count}} tipos de acción sin preguntar.", + "sensitive": "requiere cuidado", + "addonRow": "Una ejecución puede usar las acciones del complemento {{id}}.", + "blocked": "Tu agente no tiene esto, así que no se puede marcar. Añádelo antes al agente.", + "unused": "Tu agente no tiene esto, así que una ejecución no lo usa.", + "unusedList": "En la lista, pero sin uso porque el agente no lo tiene: {{names}}.", + "writesNote": "Su plan nombra el archivo {{file}}, así que una ejecución también puede escribir archivos.", + "listCan_one": "Puede actuar ({{count}})", + "listCan_other": "Puede actuar ({{count}})", + "why": { + "no_acts": "No tiene permiso para actuar, así que sus ejecuciones solo leen e informan.", + "agent": "Su agente no tiene los permisos que pide, así que sus ejecuciones solo leen e informan.", + "declared": "Se creó para leer e informar solamente. Nada de lo que marques aquí lo cambia.", + "binding": "La creó una conexión en modo de solo lectura. Nada de lo que marques aquí lo cambia." + } } } diff --git a/core/http/react-ui/public/locales/fr/agents.json b/core/http/react-ui/public/locales/fr/agents.json index 4587e03865..240d9b5072 100644 --- a/core/http/react-ui/public/locales/fr/agents.json +++ b/core/http/react-ui/public/locales/fr/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Permissions" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Permissions" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Peut utiliser {{count}} type d'action", + "acts_other": "Peut utiliser {{count}} types d'action" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/fr/chats.json b/core/http/react-ui/public/locales/fr/chats.json index 99a5225994..4ff0606baa 100644 --- a/core/http/react-ui/public/locales/fr/chats.json +++ b/core/http/react-ui/public/locales/fr/chats.json @@ -56,7 +56,8 @@ "costUnknown_other": "Les jetons d’une exécution sont connus après la première. Au plus {{count}} exécutions par jour.", "remoteNote": "{{model}} est un service distant : ce que lit une exécution quitte ce serveur.", "stopWidget": "Mettez-le en pause depuis le widget, ou supprimez-le dans Déclencheurs", - "stop": "Désactivez-le ou supprimez-le dans Déclencheurs" + "stop": "Désactivez-le ou supprimez-le dans Déclencheurs", + "actsLabel": "Permissions" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "Supprimer {{count}} fichiers", "cleared_one": "{{count}} fichier déplacé dans la corbeille.", "cleared_other": "{{count}} fichiers déplacés dans la corbeille." + }, + "triggerActs": { + "help": "Une exécution démarre seule, donc personne n'approuve une étape. Elle peut lire et rendre compte. Cochez ce qu'elle peut faire en plus. Elle n'obtient jamais plus que son agent.", + "noAgent": "Aucun agent n'est choisi, donc seul ce que vous cochez la limite.", + "readOnly": "Lecture seule", + "canAct": "Peut agir seule", + "canActText_one": "Une exécution peut utiliser {{count}} type d'action sans demander.", + "canActText_other": "Une exécution peut utiliser {{count}} types d'action sans demander.", + "sensitive": "à manier avec soin", + "addonRow": "Une exécution peut utiliser les actions du module {{id}}.", + "blocked": "Votre agent n'a pas cette permission, on ne peut donc pas la cocher. Ajoutez-la d'abord à l'agent.", + "unused": "Votre agent n'a pas cette permission, donc une exécution ne l'utilise pas.", + "unusedList": "Listée, mais inutilisée car l'agent ne l'a pas : {{names}}.", + "writesNote": "Son plan nomme le fichier {{file}}, donc une exécution peut aussi écrire des fichiers.", + "listCan_one": "Peut agir ({{count}})", + "listCan_other": "Peut agir ({{count}})", + "why": { + "no_acts": "Aucune permission d'agir n'est définie, donc ses exécutions lisent et rendent compte seulement.", + "agent": "Son agent n'a pas les permissions demandées, donc ses exécutions lisent et rendent compte seulement.", + "declared": "Il a été créé pour lire et rendre compte seulement. Rien de ce qui est coché ici ne change cela.", + "binding": "Une connexion en lecture seule l'a créé. Rien de ce qui est coché ici ne change cela." + } } } diff --git a/core/http/react-ui/public/locales/id/agents.json b/core/http/react-ui/public/locales/id/agents.json index 5bee76947e..24cf0317a6 100644 --- a/core/http/react-ui/public/locales/id/agents.json +++ b/core/http/react-ui/public/locales/id/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Izin" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Izin" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Boleh memakai {{count}} jenis tindakan", + "acts_other": "Boleh memakai {{count}} jenis tindakan" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/id/chats.json b/core/http/react-ui/public/locales/id/chats.json index 8914c8a848..2661019d17 100644 --- a/core/http/react-ui/public/locales/id/chats.json +++ b/core/http/react-ui/public/locales/id/chats.json @@ -56,7 +56,8 @@ "costUnknown_one": "Token per jalan diketahui setelah jalan pertama. Paling banyak {{count}} jalan per hari.", "remoteNote": "{{model}} adalah layanan jarak jauh: apa yang dibaca sebuah jalan meninggalkan server ini.", "stopWidget": "Jeda dari widget, atau hapus di Pemicu", - "stop": "Matikan atau hapus di Pemicu" + "stop": "Matikan atau hapus di Pemicu", + "actsLabel": "Izin" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "Hapus {{count}} berkas", "cleared_one": "{{count}} berkas dipindahkan ke Tempat Sampah.", "cleared_other": "{{count}} berkas dipindahkan ke Tempat Sampah." + }, + "triggerActs": { + "help": "Satu jalan dimulai sendiri, jadi tidak ada yang menyetujui langkahnya. Ia bisa membaca dan melapor. Centang apa yang boleh dilakukannya juga. Ia tidak pernah mendapat lebih dari yang dimiliki agennya.", + "noAgent": "Belum ada agen yang dipilih, jadi hanya centangan Anda yang membatasinya.", + "readOnly": "Hanya baca", + "canAct": "Boleh bertindak sendiri", + "canActText_one": "Satu jalan boleh memakai {{count}} jenis tindakan tanpa bertanya.", + "canActText_other": "Satu jalan boleh memakai {{count}} jenis tindakan tanpa bertanya.", + "sensitive": "perlu hati-hati", + "addonRow": "Satu jalan boleh memakai tindakan add-on {{id}}.", + "blocked": "Agen Anda tidak memiliki izin ini, jadi tidak bisa dicentang. Tambahkan dulu ke agen.", + "unused": "Agen Anda tidak memiliki izin ini, jadi satu jalan tidak memakainya.", + "unusedList": "Terdaftar, tetapi tidak dipakai karena agen tidak memilikinya: {{names}}.", + "writesNote": "Rencananya menyebut berkas {{file}}, jadi satu jalan juga boleh menulis berkas.", + "listCan_one": "Bisa bertindak ({{count}})", + "listCan_other": "Bisa bertindak ({{count}})", + "why": { + "no_acts": "Tidak ada izin bertindak, jadi jalannya hanya membaca dan melapor.", + "agent": "Agennya tidak memiliki izin yang diminta, jadi jalannya hanya membaca dan melapor.", + "declared": "Ini dibuat untuk hanya membaca dan melapor. Centangan di sini tidak mengubahnya.", + "binding": "Dibuat oleh koneksi dalam mode hanya baca. Centangan di sini tidak mengubahnya." + } } } diff --git a/core/http/react-ui/public/locales/it/agents.json b/core/http/react-ui/public/locales/it/agents.json index 7a2994b9b2..5c9e87ed6b 100644 --- a/core/http/react-ui/public/locales/it/agents.json +++ b/core/http/react-ui/public/locales/it/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Permessi" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Permessi" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Può usare {{count}} tipo di azione", + "acts_other": "Può usare {{count}} tipi di azione" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/it/chats.json b/core/http/react-ui/public/locales/it/chats.json index c7f9b38cbb..5001d6a0fc 100644 --- a/core/http/react-ui/public/locales/it/chats.json +++ b/core/http/react-ui/public/locales/it/chats.json @@ -56,7 +56,8 @@ "costUnknown_other": "I token di un'esecuzione si conoscono dopo la prima. Al massimo {{count}} esecuzioni al giorno.", "remoteNote": "{{model}} è un servizio remoto: quello che un'esecuzione legge esce da questo server.", "stopWidget": "Mettilo in pausa dal widget, o eliminalo in Trigger", - "stop": "Spegnilo o eliminalo in Trigger" + "stop": "Spegnilo o eliminalo in Trigger", + "actsLabel": "Permessi" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "Elimina {{count}} file", "cleared_one": "{{count}} file spostato nel Cestino.", "cleared_other": "{{count}} file spostati nel Cestino." + }, + "triggerActs": { + "help": "Un'esecuzione parte da sola, quindi nessuno approva un passo. Può leggere e riferire. Spunta ciò che può fare in più. Non ottiene mai più di quanto ha il suo agente.", + "noAgent": "Nessun agente scelto, quindi la limita solo ciò che spunti.", + "readOnly": "Sola lettura", + "canAct": "Può agire da sola", + "canActText_one": "Un'esecuzione può usare {{count}} tipo di azione senza chiedere.", + "canActText_other": "Un'esecuzione può usare {{count}} tipi di azione senza chiedere.", + "sensitive": "da usare con cura", + "addonRow": "Un'esecuzione può usare le azioni del componente {{id}}.", + "blocked": "Il tuo agente non ha questo permesso, quindi non si può spuntare. Aggiungilo prima all'agente.", + "unused": "Il tuo agente non ha questo permesso, quindi un'esecuzione non lo usa.", + "unusedList": "In elenco, ma non usato perché l'agente non lo ha: {{names}}.", + "writesNote": "Il suo piano nomina il file {{file}}, quindi un'esecuzione può anche scrivere file.", + "listCan_one": "Può agire ({{count}})", + "listCan_other": "Può agire ({{count}})", + "why": { + "no_acts": "Nessun permesso di agire, quindi le sue esecuzioni leggono e riferiscono soltanto.", + "agent": "Il suo agente non ha i permessi richiesti, quindi le sue esecuzioni leggono e riferiscono soltanto.", + "declared": "È stato creato per leggere e riferire soltanto. Nulla di ciò che spunti qui lo cambia.", + "binding": "L'ha creato una connessione in sola lettura. Nulla di ciò che spunti qui lo cambia." + } } } diff --git a/core/http/react-ui/public/locales/ko/agents.json b/core/http/react-ui/public/locales/ko/agents.json index 329cc3a340..439287c0fb 100644 --- a/core/http/react-ui/public/locales/ko/agents.json +++ b/core/http/react-ui/public/locales/ko/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "권한" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "권한" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "{{count}}가지 종류의 동작 사용 가능", + "acts_other": "{{count}}가지 종류의 동작 사용 가능" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/ko/chats.json b/core/http/react-ui/public/locales/ko/chats.json index f1590041e2..7bee49b8bd 100644 --- a/core/http/react-ui/public/locales/ko/chats.json +++ b/core/http/react-ui/public/locales/ko/chats.json @@ -56,7 +56,8 @@ "costUnknown_one": "실행당 토큰은 첫 실행 후에 알 수 있습니다. 하루 최대 {{count}}회.", "remoteNote": "{{model}}은(는) 원격 서비스입니다. 실행이 읽는 내용이 이 서버 밖으로 나갑니다.", "stopWidget": "위젯에서 일시 중지하거나 트리거에서 삭제하세요", - "stop": "트리거에서 끄거나 삭제하세요" + "stop": "트리거에서 끄거나 삭제하세요", + "actsLabel": "권한" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "파일 {{count}}개 삭제", "cleared_one": "파일 {{count}}개를 휴지통으로 옮겼습니다.", "cleared_other": "파일 {{count}}개를 휴지통으로 옮겼습니다." + }, + "triggerActs": { + "help": "실행은 스스로 시작되므로 단계를 승인해 줄 사람이 없습니다. 읽고 보고할 수 있습니다. 추가로 허용할 것을 선택하세요. 에이전트가 가진 것보다 많이 받지는 않습니다.", + "noAgent": "에이전트를 고르지 않았으므로 선택한 항목만 제한합니다.", + "readOnly": "읽기 전용", + "canAct": "스스로 동작 가능", + "canActText_one": "실행은 묻지 않고 {{count}}가지 종류의 동작을 쓸 수 있습니다.", + "canActText_other": "실행은 묻지 않고 {{count}}가지 종류의 동작을 쓸 수 있습니다.", + "sensitive": "주의 필요", + "addonRow": "실행은 애드온 {{id}}의 동작을 쓸 수 있습니다.", + "blocked": "에이전트에 이 권한이 없어 선택할 수 없습니다. 먼저 에이전트에 추가하세요.", + "unused": "에이전트에 이 권한이 없어 실행에서 쓰지 않습니다.", + "unusedList": "목록에 있지만 에이전트에 없어서 쓰지 않음: {{names}}.", + "writesNote": "계획에 파일 {{file}}이(가) 있어 실행이 파일도 쓸 수 있습니다.", + "listCan_one": "동작 가능 ({{count}})", + "listCan_other": "동작 가능 ({{count}})", + "why": { + "no_acts": "동작 권한이 없어 실행은 읽고 보고만 합니다.", + "agent": "에이전트에 요청한 권한이 없어 실행은 읽고 보고만 합니다.", + "declared": "읽고 보고만 하도록 만들어졌습니다. 여기서 선택해도 바뀌지 않습니다.", + "binding": "읽기 전용 모드의 연결이 만들었습니다. 여기서 선택해도 바뀌지 않습니다." + } } } diff --git a/core/http/react-ui/public/locales/pt-BR/agents.json b/core/http/react-ui/public/locales/pt-BR/agents.json index ff37baee81..c3184acb31 100644 --- a/core/http/react-ui/public/locales/pt-BR/agents.json +++ b/core/http/react-ui/public/locales/pt-BR/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "Permissões" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "Permissões" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "Pode usar {{count}} tipo de ação", + "acts_other": "Pode usar {{count}} tipos de ação" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/pt-BR/chats.json b/core/http/react-ui/public/locales/pt-BR/chats.json index 81e849ed30..18c86430dd 100644 --- a/core/http/react-ui/public/locales/pt-BR/chats.json +++ b/core/http/react-ui/public/locales/pt-BR/chats.json @@ -56,7 +56,8 @@ "costUnknown_other": "Os tokens de uma execução são conhecidos após a primeira. No máximo {{count}} execuções por dia.", "remoteNote": "{{model}} é um serviço remoto: o que uma execução lê sai deste servidor.", "stopWidget": "Pause pelo widget ou exclua em Gatilhos", - "stop": "Desligue ou exclua em Gatilhos" + "stop": "Desligue ou exclua em Gatilhos", + "actsLabel": "Permissões" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "Excluir {{count}} arquivos", "cleared_one": "{{count}} arquivo movido para a Lixeira.", "cleared_other": "{{count}} arquivos movidos para a Lixeira." + }, + "triggerActs": { + "help": "Uma execução começa sozinha, então ninguém aprova um passo. Ela pode ler e relatar. Marque o que ela também pode fazer. Ela nunca recebe mais do que o seu agente tem.", + "noAgent": "Nenhum agente escolhido, então só o que você marcar a limita.", + "readOnly": "Somente leitura", + "canAct": "Pode agir sozinha", + "canActText_one": "Uma execução pode usar {{count}} tipo de ação sem perguntar.", + "canActText_other": "Uma execução pode usar {{count}} tipos de ação sem perguntar.", + "sensitive": "requer cuidado", + "addonRow": "Uma execução pode usar as ações do complemento {{id}}.", + "blocked": "Seu agente não tem esta permissão, então ela não pode ser marcada. Adicione-a antes ao agente.", + "unused": "Seu agente não tem esta permissão, então uma execução não a usa.", + "unusedList": "Listada, mas sem uso porque o agente não a tem: {{names}}.", + "writesNote": "O plano cita o arquivo {{file}}, então uma execução também pode gravar arquivos.", + "listCan_one": "Pode agir ({{count}})", + "listCan_other": "Pode agir ({{count}})", + "why": { + "no_acts": "Sem permissão para agir, então suas execuções só leem e relatam.", + "agent": "Seu agente não tem as permissões pedidas, então suas execuções só leem e relatam.", + "declared": "Foi criado para apenas ler e relatar. Nada que você marque aqui muda isso.", + "binding": "Foi criado por uma conexão em modo somente leitura. Nada que você marque aqui muda isso." + } } } diff --git a/core/http/react-ui/public/locales/zh-CN/agents.json b/core/http/react-ui/public/locales/zh-CN/agents.json index e6072793a4..80f4a5b998 100644 --- a/core/http/react-ui/public/locales/zh-CN/agents.json +++ b/core/http/react-ui/public/locales/zh-CN/agents.json @@ -218,7 +218,8 @@ "watches": "Watches the folder {{folder}}", "noRuns": "This trigger has not run yet.", "runMeta": "took {{time}}", - "prompt": "What to do" + "prompt": "What to do", + "acts": "权限" }, "edit": { "titleNew": "New trigger", @@ -234,7 +235,8 @@ "basics": "Basics", "prompt": "What to do", "schedule": "When", - "limits": "Limits" + "limits": "Limits", + "acts": "权限" }, "summary": { "basics": "{{name}}, {{model}}", @@ -242,7 +244,9 @@ "promptEmpty": "Nothing to do yet", "folderEmpty": "Needs a folder", "cap": "Up to {{count}} runs a day", - "capDefault": "Default limit" + "capDefault": "Default limit", + "acts_one": "可使用 {{count}} 类操作", + "acts_other": "可使用 {{count}} 类操作" }, "name": "Name", "namePlaceholder": "daily-digest", diff --git a/core/http/react-ui/public/locales/zh-CN/chats.json b/core/http/react-ui/public/locales/zh-CN/chats.json index 3f2fe9adc5..7913ea9c42 100644 --- a/core/http/react-ui/public/locales/zh-CN/chats.json +++ b/core/http/react-ui/public/locales/zh-CN/chats.json @@ -56,7 +56,8 @@ "costUnknown_one": "每次运行的 token 数在首次运行后可知。每天最多 {{count}} 次。", "remoteNote": "{{model}} 是远程服务:运行读取的内容会离开此服务器。", "stopWidget": "在小组件中暂停,或在触发器中删除", - "stop": "在触发器中关闭或删除" + "stop": "在触发器中关闭或删除", + "actsLabel": "权限" } }, "mcpPage": { @@ -730,5 +731,27 @@ "clearYes_other": "删除 {{count}} 个文件", "cleared_one": "已将 {{count}} 个文件移到回收站。", "cleared_other": "已将 {{count}} 个文件移到回收站。" + }, + "triggerActs": { + "help": "运行会自行开始,没有人批准每一步。它可以读取并汇报。勾选它还可以做什么。它得到的权限不会超过其代理。", + "noAgent": "未选择代理,因此只有你勾选的项目起限制作用。", + "readOnly": "只读", + "canAct": "可自行操作", + "canActText_one": "一次运行可在不询问的情况下使用 {{count}} 类操作。", + "canActText_other": "一次运行可在不询问的情况下使用 {{count}} 类操作。", + "sensitive": "需谨慎", + "addonRow": "一次运行可使用附加组件 {{id}} 的操作。", + "blocked": "你的代理没有此权限,因此无法勾选。请先将它添加到代理。", + "unused": "你的代理没有此权限,因此运行不会使用它。", + "unusedList": "已列出,但代理没有,所以不使用:{{names}}。", + "writesNote": "其计划指明了文件 {{file}},因此一次运行也可以写入文件。", + "listCan_one": "可操作({{count}})", + "listCan_other": "可操作({{count}})", + "why": { + "no_acts": "未设置操作权限,因此其运行只读取并汇报。", + "agent": "其代理没有它要求的权限,因此其运行只读取并汇报。", + "declared": "它被设置为只读取并汇报。在此勾选不会改变这一点。", + "binding": "它由只读模式的连接创建。在此勾选不会改变这一点。" + } } } diff --git a/core/http/react-ui/src/components/TriggerActs.jsx b/core/http/react-ui/src/components/TriggerActs.jsx new file mode 100644 index 0000000000..84ae3aa884 --- /dev/null +++ b/core/http/react-ui/src/components/TriggerActs.jsx @@ -0,0 +1,114 @@ +import { useTranslation } from 'react-i18next' +import { useBuiltinGrants } from '../hooks/useBuiltinGrants' +import { actNamed, actRows, actsAfter, actsStatus, actsStatusOf, actCatalog } from '../utils/triggerActs' +import { Callout } from './ui' +import './trigger-acts.css' + +// What the runs of a trigger may do with nobody present. Three views of the +// same rules (utils/triggerActs.js): +// ActsEditor the checklist of the trigger form, drawn like the grants of an +// agent: a plain-words line on each, sensitive ones marked +// ActsSummary the read only view of a trigger the server returned (details +// page, proposal card) +// ReadOnlyTag a short mark for a list row +// A run that holds no act only reads and reports. The server enforces this at +// each tool call; the interface only shows it. + +function Tag({ children }) { + return {children} +} + +/** One short reason why a run only reads, in plain words. */ +function ReadOnlyWhy({ why, t }) { + return <>{t(`triggerActs.why.${why || 'no_acts'}`)}> +} + +/** + * The editable list. value is the acts of the form, agentGrants the grants of + * the chosen agent (null: no agent chosen), writes the file the plan names, + * lock why the trigger is read only whatever is ticked. + */ +// eslint-disable-next-line no-unused-vars -- used in JSX; this config has no eslint-plugin-react +export function ActsEditor({ value, onChange, agentGrants, writes = '', lock = '' }) { + const { t } = useTranslation('chats') + const catalog = actCatalog(useBuiltinGrants()) + const rows = actRows(catalog, value, agentGrants) + const status = actsStatus({ acts: value, agentGrants, writes, lock }) + return ( +
{t('triggerActs.help')}
+ {agentGrants == null &&{t('triggerActs.noAgent')}
} +{t('triggerActs.writesNote', { file: writes })}
} +{t('triggerActs.unusedList', { names: unused.map((g) => g.title).join(', ') })}
+ )} +{p.plan.writes}{t('proposal.plan.writesWidget', { widget: widget.title })}>
: {p.plan.writes}],
['cost', costText],
+ ['acts',