diff --git a/.changeset/chat-stream-mid-turn-reconnect.md b/.changeset/chat-stream-mid-turn-reconnect.md new file mode 100644 index 00000000000..a73c619bd80 --- /dev/null +++ b/.changeset/chat-stream-mid-turn-reconnect.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/core": patch +"@trigger.dev/sdk": patch +--- + +Chat in the browser now reconnects when the connection drops mid-turn, instead of leaving the reply stuck as if it were still generating. diff --git a/.changeset/mint-token-cap-help.md b/.changeset/mint-token-cap-help.md new file mode 100644 index 00000000000..432bd34413c --- /dev/null +++ b/.changeset/mint-token-cap-help.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +`trigger mint-token --help` now says that a token minted without `--cap` is read-only, instead of claiming it carries your full role. diff --git a/.changeset/report-json-and-period-units.md b/.changeset/report-json-and-period-units.md new file mode 100644 index 00000000000..5567d2a5b9b --- /dev/null +++ b/.changeset/report-json-and-period-units.md @@ -0,0 +1,6 @@ +--- +"@trigger.dev/core": patch +"trigger.dev": patch +--- + +Reports can be fetched as structured data with the `json` format. The shortest report period is now one minute (`30m`, `1h`, `7d`). diff --git a/.changeset/uat-ttl-help-text.md b/.changeset/uat-ttl-help-text.md new file mode 100644 index 00000000000..01aa6a79e3a --- /dev/null +++ b/.changeset/uat-ttl-help-text.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +The `mint-token` command's `--ttl` help now shows the correct maximum token lifetime of 7 days. diff --git a/.claude/skills/errors-api-e2e/SKILL.md b/.claude/skills/errors-api-e2e/SKILL.md index e74c50bf5ea..162526ee445 100644 --- a/.claude/skills/errors-api-e2e/SKILL.md +++ b/.claude/skills/errors-api-e2e/SKILL.md @@ -154,7 +154,7 @@ PASS: one run, `run_` (status maps to `FAILED`). Proves `filter[error]` -> ### 6. Attribution — `mint-token` -> JWT exchange records the acting user ```bash -TOKEN=$(cli mint-token --profile $PROFILE --client errors-api-e2e 2>/dev/null) # UAT +TOKEN=$(cli mint-token --profile $PROFILE --client errors-api-e2e --cap read:errors,write:errors 2>/dev/null) # UAT ENVJWT=$(curl -sS -X POST "$B/api/v1/projects/$REF/dev/jwt" -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"claims":{"scopes":["read:errors","write:errors"]}}' \ | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])") diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..e9b96126a80 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Generated, not hand-written: collapsed in diffs and excluded from language stats. +internal-packages/dashboard-agent-db/drizzle/meta/*.json linguist-generated=true +**/__snapshots__/*.snap linguist-generated=true +pnpm-lock.yaml linguist-generated=true diff --git a/.gitignore b/.gitignore index f540927e32b..b11dded2b02 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ ailogger-output.log # observability-map CLI output artifact, not committed observability-map.json + +.claude/worktrees/ diff --git a/.server-changes/agent-body-cap-task-id.md b/.server-changes/agent-body-cap-task-id.md new file mode 100644 index 00000000000..92213979289 --- /dev/null +++ b/.server-changes/agent-body-cap-task-id.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Triggering a task whose id contains "dashboard-agent" is no longer subject to the smaller chat request size limit, so larger payloads go through as expected. diff --git a/.server-changes/agent-env-jwt-scope-ceiling.md b/.server-changes/agent-env-jwt-scope-ceiling.md new file mode 100644 index 00000000000..8173e45b2be --- /dev/null +++ b/.server-changes/agent-env-jwt-scope-ceiling.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Delegated read-only tokens can no longer be exchanged for a token with broader access. diff --git a/.server-changes/agent-env-jwt-ttl-clamp.md b/.server-changes/agent-env-jwt-ttl-clamp.md new file mode 100644 index 00000000000..105bca0e892 --- /dev/null +++ b/.server-changes/agent-env-jwt-ttl-clamp.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +A delegated token can no longer be exchanged for a longer-lived token than itself. diff --git a/.server-changes/agent-response-remote-images.md b/.server-changes/agent-response-remote-images.md new file mode 100644 index 00000000000..b96426b4396 --- /dev/null +++ b/.server-changes/agent-response-remote-images.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Images in AI and agent responses are no longer loaded from arbitrary remote websites, closing a way a response could quietly signal an outside server. diff --git a/.server-changes/agent-untrusted-text-fence.md b/.server-changes/agent-untrusted-text-fence.md new file mode 100644 index 00000000000..7f8f28f8b51 --- /dev/null +++ b/.server-changes/agent-untrusted-text-fence.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +The dashboard agent now treats captured content it reads — run logs, error messages, and commit messages — as data, so instructions hidden inside them can no longer steer the assistant. diff --git a/.server-changes/dashboard-agent-chat-retention.md b/.server-changes/dashboard-agent-chat-retention.md new file mode 100644 index 00000000000..82d810fbd10 --- /dev/null +++ b/.server-changes/dashboard-agent-chat-retention.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Deleted assistant conversations are now permanently removed after a grace period, and deleting an organization also removes its conversations. diff --git a/.server-changes/dashboard-agent.md b/.server-changes/dashboard-agent.md new file mode 100644 index 00000000000..eaadfafe915 --- /dev/null +++ b/.server-changes/dashboard-agent.md @@ -0,0 +1,16 @@ +--- +area: webapp +type: feature +--- + +Meet the dashboard agent: a chat in every environment that answers questions about your runs, queues, errors and health with real data and links. It takes over from Ask AI everywhere that used to appear; on the Free plan you get 20 messages. + +**Investigate** on a failed run, an error, a backed-up queue or a run that hasn't started gets you a worked-through answer — what happened, why, and how to fix it, with every claim linked to the runs, errors and deploys behind it. + +The agent works on preview and dev branches, with that branch's own data. + +The health report reads the same everywhere — dashboard, terminal, editor. A very long chat keeps working: the agent summarises the earlier part and carries on. The agent's replies no longer show images. + +A sample of conversations is scored automatically so the agent keeps getting better. Only the score and a one-line summary are kept, never your messages, data or code, and we can switch it off for your organization on request. + +Separately: a queue's wait times, peak depth, throughput and throttling can now be read from the API, and the Docs button has been removed from page headers. diff --git a/.server-changes/org-delete-agent-purge-best-effort.md b/.server-changes/org-delete-agent-purge-best-effort.md new file mode 100644 index 00000000000..01d6831fa72 --- /dev/null +++ b/.server-changes/org-delete-agent-purge-best-effort.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Deleting an organization now always succeeds, even if a temporary background hiccup prevents its agent chats from being cleaned up right away. diff --git a/.server-changes/report-freshness-window.md b/.server-changes/report-freshness-window.md new file mode 100644 index 00000000000..807c450cb22 --- /dev/null +++ b/.server-changes/report-freshness-window.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Health reports no longer show as "fresh" when the underlying data is actually older than the freshness window. diff --git a/.server-changes/user-actor-token-lifetime-and-revocation.md b/.server-changes/user-actor-token-lifetime-and-revocation.md new file mode 100644 index 00000000000..56ee449507c --- /dev/null +++ b/.server-changes/user-actor-token-lifetime-and-revocation.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Revoking a personal access token now also stops any short-lived tokens created from it. Tokens created from a personal access token can now live for at most 7 days. diff --git a/apps/webapp/.gitignore b/apps/webapp/.gitignore index 595ab180e15..f825411d640 100644 --- a/apps/webapp/.gitignore +++ b/apps/webapp/.gitignore @@ -7,6 +7,9 @@ node_modules /cypress/screenshots /cypress/videos +# Output of `pnpm run agent-ui:screenshots` +/screenshots + /app/styles/tailwind.css # Ensure the .env symlink is not removed by accident @@ -20,4 +23,4 @@ storybook-static /prisma/seed.js /prisma/populate.js -.memory-snapshots \ No newline at end of file +.memory-snapshots diff --git a/apps/webapp/app/components/AskAI.tsx b/apps/webapp/app/components/AskAI.tsx index d61ea0055fa..389d5e9e569 100644 --- a/apps/webapp/app/components/AskAI.tsx +++ b/apps/webapp/app/components/AskAI.tsx @@ -1,3 +1,9 @@ +/** + * @deprecated Superseded by the dashboard agent (`components/dashboard-agent`). Nothing mounts + * this any more — every Ask AI entry point now opens Ask Trigger. Kept until the agent has + * shipped, then removed along with `@kapaai/react-sdk` and `KAPA_AI_WEBSITE_ID`. + */ + import { ArrowPathIcon, ArrowUpIcon, @@ -81,6 +87,8 @@ function useAskAIState() { * it around the popover, not inside, so the dialog and shortcut survive the popover closing. * `children` receives the open function, or undefined when Ask AI is unavailable (self-hosted, no * Kapa website id, or SSR). + * + * @deprecated See the note at the top of this file. */ export function AskAIRoot({ children, @@ -137,6 +145,7 @@ function AskAIRootProvider({ ); } +/** @deprecated See the note at the top of this file. */ export function AskAI({ isCollapsed = false }: { isCollapsed?: boolean }) { const { isManagedCloud } = useFeatures(); const websiteId = useKapaWebsiteId(); diff --git a/apps/webapp/app/components/code/StreamdownRenderer.test.ts b/apps/webapp/app/components/code/StreamdownRenderer.test.ts new file mode 100644 index 00000000000..994f82ff26e --- /dev/null +++ b/apps/webapp/app/components/code/StreamdownRenderer.test.ts @@ -0,0 +1,80 @@ +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { restrictModelUrls, StreamdownRenderer } from "./StreamdownRenderer"; + +// streamdown calls urlTransform(url, key, node) to compute each url attribute; a +// returned undefined removes the attribute, so no request is ever issued. +const img = { tagName: "img" } as any; +const link = { tagName: "a" } as any; + +describe("restrictModelUrls (image src)", () => { + it("drops a remote model-authored image (the favicon beacon)", () => { + expect( + restrictModelUrls("https://www.google.com/s2/favicons?domain=evil", "src", img) + ).toBeUndefined(); + }); + + it("drops any absolute or protocol-relative remote image", () => { + expect(restrictModelUrls("http://evil.tld/pixel.gif", "src", img)).toBeUndefined(); + expect(restrictModelUrls("//evil.tld/pixel.gif", "src", img)).toBeUndefined(); + }); + + it("keeps inline and same-origin images", () => { + expect(restrictModelUrls("data:image/png;base64,AAAA", "src", img)).toBe( + "data:image/png;base64,AAAA" + ); + expect(restrictModelUrls("blob:abc", "src", img)).toBe("blob:abc"); + expect(restrictModelUrls("/local/pic.png", "src", img)).toBe("/local/pic.png"); + }); +}); + +describe("restrictModelUrls (link href)", () => { + it("keeps http(s), mailto and relative links", () => { + expect(restrictModelUrls("https://trigger.dev/docs", "href", link)).toBe( + "https://trigger.dev/docs" + ); + expect(restrictModelUrls("http://example.com", "href", link)).toBe("http://example.com"); + expect(restrictModelUrls("mailto:hi@trigger.dev", "href", link)).toBe("mailto:hi@trigger.dev"); + expect(restrictModelUrls("/runs/123", "href", link)).toBe("/runs/123"); + }); + + it("drops unsafe link schemes", () => { + expect(restrictModelUrls("javascript:alert(1)", "href", link)).toBeUndefined(); + expect(restrictModelUrls("data:text/html,