From a125926f98bb46f5de34d9510a53edf5fcafc513 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 20 Aug 2026 14:52:05 +0100 Subject: [PATCH] fix(webapp): keep the dashboard agent's tool calls on the user's instance Follow-up to #4738. That change routed the agent's session (start, token, transport, realtime) to a configurable instance, but it also moved the origin the agent's read-tools call back to. Those tools act as the user with a delegated token this webapp signs with its own SESSION_SECRET, scoped to its own user and environment, so the token can only be verified by, and the data only found on, that same instance. Split the two origins. dashboardAgentApiOrigin() stays the instance hosting the agent project (sessions, task triggers, realtime, the in forward). A new dashboardAgentUserApiOrigin() returns this webapp's own origin and is injected into the run metadata the tools use. When the agent runs on the same instance as the webapp both resolve to the same host, so nothing changes there. --- ...$projectParam.env.$envParam.dashboard-agent.in.$.ts | 4 +++- ...ects.$projectParam.env.$envParam.dashboard-agent.ts | 4 ++-- apps/webapp/app/services/dashboardAgent.server.ts | 6 ++++-- .../services/dashboardAgentWatchInvestigate.server.ts | 4 +++- .../app/services/dashboardAgentWatches.server.ts | 10 +++++++--- apps/webapp/test/dashboardAgentClientMetadata.test.ts | 1 + .../test/dashboardAgentCreateChatOrdering.test.ts | 3 ++- apps/webapp/test/dashboardAgentForeignChat.test.ts | 1 + .../test/dashboardAgentInProxyMintFailure.test.ts | 1 + .../webapp/test/dashboardAgentWatchInvestigate.test.ts | 1 + 10 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.in.$.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.in.$.ts index 8173aae5761..2b57c80b414 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.in.$.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.in.$.ts @@ -12,6 +12,7 @@ import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { dashboardAgentApiOrigin, + dashboardAgentUserApiOrigin, mintDashboardAgentUserActorToken, resolveDashboardAgentRepoSnapshot, } from "~/services/dashboardAgent.server"; @@ -92,6 +93,7 @@ export async function action({ request, params }: ActionFunctionArgs) { if (!upstreamPath) return json({ error: "Not found" }, { status: 404 }); const apiOrigin = dashboardAgentApiOrigin(); + const userApiOrigin = dashboardAgentUserApiOrigin(); const url = new URL(request.url); const upstreamUrl = `${apiOrigin.replace(/\/$/, "")}/${upstreamPath}${url.search}`; @@ -167,7 +169,7 @@ export async function action({ request, params }: ActionFunctionArgs) { parsed.payload.metadata = { ...pickAgentClientMetadata(parsed.payload.metadata), userActorToken, - apiOrigin, + apiOrigin: userApiOrigin, projectRef: project.externalRef, // Server-owned: the eval opt-out and every tenancy check key on these. organizationId: project.organizationId, diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts index 89677bf43d5..3564b42849d 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts @@ -42,7 +42,7 @@ import { submitDashboardAgentWatch, } from "~/services/dashboardAgentWatches.server"; import { - dashboardAgentApiOrigin, + dashboardAgentUserApiOrigin, dashboardAgentWakeFeedCounter, isDashboardAgentConfigured, mintDashboardAgentToken, @@ -345,7 +345,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { userActorToken: await mintDashboardAgentUserActorToken(userId, { environmentId: runtimeEnv.id, }), - apiOrigin: dashboardAgentApiOrigin(), + apiOrigin: dashboardAgentUserApiOrigin(), projectRef: project.externalRef, // Server-owned, like the `in` proxy: the eval opt-out and every tenancy check // key on these, so the client can't set them at all. diff --git a/apps/webapp/app/services/dashboardAgent.server.ts b/apps/webapp/app/services/dashboardAgent.server.ts index 96b7eea73ce..5ff460ae33f 100644 --- a/apps/webapp/app/services/dashboardAgent.server.ts +++ b/apps/webapp/app/services/dashboardAgent.server.ts @@ -46,12 +46,14 @@ export const DASHBOARD_AGENT_UAT_CAP = [ // the agent's run payload expires quickly. const DASHBOARD_AGENT_UAT_TTL_SECONDS = 10 * 60; -// The Trigger instance this webapp runs against — the same origin the agent -// task calls back to (as the user) for its read tools. export function dashboardAgentApiOrigin(): string { return env.DASHBOARD_AGENT_BASE_URL ?? "https://api.trigger.dev"; } +export function dashboardAgentUserApiOrigin(): string { + return env.API_ORIGIN ?? env.APP_ORIGIN; +} + // Mint a short-lived, read-only delegated token for the signed-in user. Self // service from the dashboard session (never a PAT), so a user can only ever // mint a token for themselves. The `in` proxy injects this into the turn's diff --git a/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts b/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts index 96ae8444f19..53277a91400 100644 --- a/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatchInvestigate.server.ts @@ -10,6 +10,7 @@ import { ApiClient } from "@trigger.dev/core/v3"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { dashboardAgentApiOrigin, + dashboardAgentUserApiOrigin, mintDashboardAgentUserActorToken, } from "~/services/dashboardAgent.server"; import { dashboardAgentEnvironmentAddress } from "~/services/dashboardAgentEnvironmentAddress.server"; @@ -57,6 +58,7 @@ export async function kickWatchInvestigation(params: { if (!accessToken) throw new Error("DASHBOARD_AGENT_SECRET_KEY is not set"); const apiOrigin = dashboardAgentApiOrigin(); + const userApiOrigin = dashboardAgentUserApiOrigin(); // The watch's immutable tenancy plus the delegated token that lets the turn read. const metadata = { userId: watch.userId, @@ -65,7 +67,7 @@ export async function kickWatchInvestigation(params: { environmentId: watch.environmentId, projectRef: watch.projectRef ?? environment.project.externalRef, ...dashboardAgentEnvironmentAddress(environment), - apiOrigin, + apiOrigin: userApiOrigin, userActorToken: await mintDashboardAgentUserActorToken(watch.userId, { environmentId: watch.environmentId, }), diff --git a/apps/webapp/app/services/dashboardAgentWatches.server.ts b/apps/webapp/app/services/dashboardAgentWatches.server.ts index ceb5309304e..e341f8ee51d 100644 --- a/apps/webapp/app/services/dashboardAgentWatches.server.ts +++ b/apps/webapp/app/services/dashboardAgentWatches.server.ts @@ -56,6 +56,7 @@ import { authIncludeWithParent, toAuthenticated } from "~/models/runtimeEnvironm import { isReportKey } from "~/presenters/v3/reports/report-registry"; import { dashboardAgentApiOrigin, + dashboardAgentUserApiOrigin, isDashboardAgentConfigured as isDashboardAgentConfiguredDefault, } from "~/services/dashboardAgent.server"; import { dashboardAgentDb } from "~/services/dashboardAgentDb.server"; @@ -1043,11 +1044,12 @@ export async function scheduleWatchTick(params: { if (!accessToken) throw new Error("DASHBOARD_AGENT_SECRET_KEY is not set"); const apiOrigin = dashboardAgentApiOrigin(); + const userApiOrigin = dashboardAgentUserApiOrigin(); const client = new TriggerClient({ baseURL: apiOrigin, accessToken }); await client.tasks.trigger( WATCH_TASK_ID, - { watchId: params.watchId, token: params.token, apiOrigin, tick: params.tick }, + { watchId: params.watchId, token: params.token, apiOrigin: userApiOrigin, tick: params.tick }, { delay: `${params.delayMinutes}m`, // Keyed on the generation the payload carries, so a retried schedule can't double-tick. @@ -1138,6 +1140,7 @@ export async function scheduleWatchBatchTick(params: { if (!accessToken) throw new Error("DASHBOARD_AGENT_SECRET_KEY is not set"); const apiOrigin = dashboardAgentApiOrigin(); + const userApiOrigin = dashboardAgentUserApiOrigin(); const client = new TriggerClient({ baseURL: apiOrigin, accessToken }); const token = await mintDashboardAgentWatchBatchToken({ environmentId: params.environmentId, @@ -1149,7 +1152,7 @@ export async function scheduleWatchBatchTick(params: { { environmentId: params.environmentId, cadenceMinutes: params.cadenceMinutes, - apiOrigin, + apiOrigin: userApiOrigin, token, epoch: params.epoch, tick: params.tick, @@ -1173,6 +1176,7 @@ export async function scheduleWatchDelivery(watch: { id: string; expiresAt: Date if (!accessToken) throw new Error("DASHBOARD_AGENT_SECRET_KEY is not set"); const apiOrigin = dashboardAgentApiOrigin(); + const userApiOrigin = dashboardAgentUserApiOrigin(); const client = new TriggerClient({ baseURL: apiOrigin, accessToken }); const token = await mintDashboardAgentWatchToken({ watchId: watch.id, @@ -1181,7 +1185,7 @@ export async function scheduleWatchDelivery(watch: { id: string; expiresAt: Date await client.tasks.trigger( WATCH_TASK_ID, - { watchId: watch.id, token, apiOrigin, tick: 0, deliverOnly: true }, + { watchId: watch.id, token, apiOrigin: userApiOrigin, tick: 0, deliverOnly: true }, { idempotencyKey: `watch:${watch.id}:deliver`, idempotencyKeyTTL: "10m", diff --git a/apps/webapp/test/dashboardAgentClientMetadata.test.ts b/apps/webapp/test/dashboardAgentClientMetadata.test.ts index da4e9c10a8b..b0c065d03ce 100644 --- a/apps/webapp/test/dashboardAgentClientMetadata.test.ts +++ b/apps/webapp/test/dashboardAgentClientMetadata.test.ts @@ -27,6 +27,7 @@ vi.mock("~/models/runtimeEnvironment.server", () => ({ })); vi.mock("~/services/dashboardAgent.server", () => ({ dashboardAgentApiOrigin: () => "https://api.trigger.dev", + dashboardAgentUserApiOrigin: () => "https://api.trigger.dev", isDashboardAgentConfigured: () => true, mintDashboardAgentToken: async () => "pat_public", mintDashboardAgentUserActorToken: async () => "tr_uat_real", diff --git a/apps/webapp/test/dashboardAgentCreateChatOrdering.test.ts b/apps/webapp/test/dashboardAgentCreateChatOrdering.test.ts index a30c0a9d945..d464900307b 100644 --- a/apps/webapp/test/dashboardAgentCreateChatOrdering.test.ts +++ b/apps/webapp/test/dashboardAgentCreateChatOrdering.test.ts @@ -35,7 +35,8 @@ vi.mock("~/models/runtimeEnvironment.server", () => ({ findEnvironmentBySlug: mocks.findEnvironmentBySlug, })); vi.mock("~/services/dashboardAgent.server", () => ({ - dashboardAgentApiOrigin: () => "https://api.trigger.dev", + dashboardAgentApiOrigin: () => "https://agent.trigger.dev", + dashboardAgentUserApiOrigin: () => "https://api.trigger.dev", isDashboardAgentConfigured: () => true, mintDashboardAgentToken: mocks.mintPublicToken, mintDashboardAgentUserActorToken: mocks.mintUserActorToken, diff --git a/apps/webapp/test/dashboardAgentForeignChat.test.ts b/apps/webapp/test/dashboardAgentForeignChat.test.ts index 6bcaf3c5d6a..2fb0785fdcd 100644 --- a/apps/webapp/test/dashboardAgentForeignChat.test.ts +++ b/apps/webapp/test/dashboardAgentForeignChat.test.ts @@ -26,6 +26,7 @@ vi.mock("~/models/project.server", () => ({ vi.mock("~/models/runtimeEnvironment.server", () => ({ findEnvironmentBySlug: vi.fn() })); vi.mock("~/services/dashboardAgent.server", () => ({ dashboardAgentApiOrigin: () => "https://api.trigger.dev", + dashboardAgentUserApiOrigin: () => "https://api.trigger.dev", isDashboardAgentConfigured: () => true, mintDashboardAgentToken: vi.fn(), mintDashboardAgentUserActorToken: vi.fn(), diff --git a/apps/webapp/test/dashboardAgentInProxyMintFailure.test.ts b/apps/webapp/test/dashboardAgentInProxyMintFailure.test.ts index fb24ef87b45..3304e631d6e 100644 --- a/apps/webapp/test/dashboardAgentInProxyMintFailure.test.ts +++ b/apps/webapp/test/dashboardAgentInProxyMintFailure.test.ts @@ -25,6 +25,7 @@ vi.mock("~/models/runtimeEnvironment.server", () => ({ })); vi.mock("~/services/dashboardAgent.server", () => ({ dashboardAgentApiOrigin: () => "https://api.trigger.dev", + dashboardAgentUserApiOrigin: () => "https://api.trigger.dev", mintDashboardAgentUserActorToken: mocks.mint, resolveDashboardAgentRepoSnapshot: async () => null, })); diff --git a/apps/webapp/test/dashboardAgentWatchInvestigate.test.ts b/apps/webapp/test/dashboardAgentWatchInvestigate.test.ts index 599f3ab6d8c..2ebf04f02c9 100644 --- a/apps/webapp/test/dashboardAgentWatchInvestigate.test.ts +++ b/apps/webapp/test/dashboardAgentWatchInvestigate.test.ts @@ -46,6 +46,7 @@ vi.mock("~/services/dashboardAgentWatches.server", () => ({ const mints = vi.hoisted(() => [] as Array<{ userId: string; environmentId?: string }>); vi.mock("~/services/dashboardAgent.server", () => ({ dashboardAgentApiOrigin: () => "https://api.example.com", + dashboardAgentUserApiOrigin: () => "https://api.example.com", dashboardAgentEnvironmentName: (type: string | undefined) => type === "PRODUCTION" ? "prod" : undefined, mintDashboardAgentUserActorToken: async (