This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5605-539a68551288a951d4c0a6d262105a8e6c211790
in repository https://gitbox.apache.org/repos/asf/texera.git

commit ae3128d999c48d0f014be2eea2eb4fc0f4ee9e66
Author: Jiadong Bai <[email protected]>
AuthorDate: Wed Jun 10 23:32:48 2026 -0700

    fix(agent-service): authenticate to the LLM proxy as the delegating user 
(#5605)
    
    ### What changes were proposed in this PR?
    
    Since #5421, the access-control-service LLM gateway requires a
    REGULAR/ADMIN-role user JWT and injects `LITELLM_MASTER_KEY` into the
    downstream request itself, but the agent-service still authenticated
    with the static `LLM_API_KEY` default (`"dummy"`), so every agent
    generation call returned 401 Unauthorized. This PR makes the delegating
    user's JWT the only credential the agent-service ever sends:
    
    - Creating an agent (`POST /agents`) now requires the user's JWT in the
    `Authorization: Bearer` header (the standard place for credentials,
    instead of a `userToken` field in the JSON payload); requests without it
    are rejected with 401, since an agent without a user could never call
    the gateway anyway. The frontend's JWT interceptor already attaches this
    header, so `agent.service.ts` simply stops duplicating the token in the
    request body.
    - The OpenAI client authenticates with the delegating user's JWT; the
    `LLM_API_KEY` env var is removed from the service (`env.ts`), the helm
    chart (deployment env, `llm-api-key` secret entry, `llmApiKey` values),
    and the single-node `.env`. The master key now lives only in the
    access-control-service and LiteLLM.
    - Elysia `VALIDATION`/`PARSE` errors are mapped to 400 instead of
    falling through to a generic 500 (surfaced by Copilot's review comment
    on the schema-violation tests).
    
    ```mermaid
    sequenceDiagram
        participant FE as Frontend (user JWT)
        participant AS as agent-service
        participant ACS as access-control-service (LLM gateway)
        participant LLM as LiteLLM
    
        FE->>AS: POST /agents<br/>Authorization: Bearer (user JWT) — required, 
else 401
        FE->>AS: send message
        rect rgb(255, 235, 235)
        Note over AS,ACS: before: Authorization: Bearer dummy → 401
        end
        AS->>ACS: POST /api/chat/completions<br/>Authorization: Bearer (user 
JWT) ✅
        ACS->>LLM: forward with Authorization: Bearer (LITELLM_MASTER_KEY)
        LLM-->>AS: completion
        AS-->>FE: agent response
    ```
    
    ### Any related issues, documentation, discussions?
    
    Closes #5604
    
    ### How was this PR tested?
    
    Updated `server.test.ts`: agent creation sends a minted JWT in the
    `Authorization` header; added cases for a missing header, a non-Bearer
    scheme, and an invalid token (93 tests pass via `bun test`). Also
    verified end-to-end on a local Kubernetes deployment (this branch's
    chart + agent-service image, everything else from `ghcr.io/apache`
    nightlies): creation without the header → 401, with the header → 200,
    and a websocket message produced a real `gpt-5-mini` completion through
    access-control-service → LiteLLM. `typecheck` and `format:check` pass;
    the frontend change typechecks via `tsc --noEmit`.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Fable 5 (1M context)
    
    ---------
    
    Co-authored-by: Bob Bai <[email protected]>
---
 agent-service/src/api/auth-api.ts                  |  6 ++
 agent-service/src/config/env.ts                    |  1 -
 agent-service/src/server.test.ts                   | 74 +++++++++++------
 agent-service/src/server.ts                        | 78 ++++++++++--------
 agent-service/src/types/agent.ts                   |  1 -
 bin/k8s/templates/agent-service-deployment.yaml    |  5 --
 bin/k8s/templates/agent-service-secret.yaml        | 11 +--
 bin/k8s/values-development.yaml                    |  4 -
 bin/k8s/values.yaml                                |  4 -
 bin/single-node/.env                               |  1 -
 .../workspace/service/agent/agent.service.spec.ts  | 95 ++++++++++++++++++++++
 .../app/workspace/service/agent/agent.service.ts   | 24 ++----
 12 files changed, 209 insertions(+), 95 deletions(-)

diff --git a/agent-service/src/api/auth-api.ts 
b/agent-service/src/api/auth-api.ts
index 087f93ac46..4a166269c7 100644
--- a/agent-service/src/api/auth-api.ts
+++ b/agent-service/src/api/auth-api.ts
@@ -57,6 +57,12 @@ export function validateToken(token: string): boolean {
   return !isTokenExpired(token);
 }
 
+export function extractBearerToken(header: string | undefined): string | 
undefined {
+  if (!header) return undefined;
+  const [scheme, token] = header.split(" ");
+  return scheme?.toLowerCase() === "bearer" && token ? token : undefined;
+}
+
 export function createAuthHeaders(token: string): Record<string, string> {
   return {
     Authorization: `Bearer ${token}`,
diff --git a/agent-service/src/config/env.ts b/agent-service/src/config/env.ts
index 16a25b9be7..3513286b14 100644
--- a/agent-service/src/config/env.ts
+++ b/agent-service/src/config/env.ts
@@ -22,7 +22,6 @@ import { z } from "zod";
 const EnvSchema = z.object({
   PORT: z.coerce.number().default(3001),
   API_PREFIX: z.string().default("/api"),
-  LLM_API_KEY: z.string().default("dummy"),
   TEXERA_SERVICE_LOG_LEVEL: z
     .enum(["ERROR", "WARN", "INFO", "DEBUG"])
     .transform(v => v.toLowerCase() as "error" | "warn" | "info" | "debug")
diff --git a/agent-service/src/server.test.ts b/agent-service/src/server.test.ts
index 0f618e599c..b8de8736bd 100644
--- a/agent-service/src/server.test.ts
+++ b/agent-service/src/server.test.ts
@@ -24,20 +24,40 @@ import { env } from "./config/env";
 const API = env.API_PREFIX;
 const app = buildApp();
 
+function mintTestToken(): string {
+  const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" 
})).toString("base64url");
+  const payload = Buffer.from(
+    JSON.stringify({
+      sub: "tester",
+      userId: 1,
+      email: "[email protected]",
+      role: "REGULAR",
+      exp: Math.floor(Date.now() / 1000) + 3600,
+    })
+  ).toString("base64url");
+  return `${header}.${payload}.test-signature`;
+}
+
+const TOKEN = mintTestToken();
+
 function url(path: string): string {
   return `http://localhost${path}`;
 }
 
-async function postJson(path: string, body: unknown): Promise<Response> {
+async function postJson(path: string, body: unknown, headers: Record<string, 
string> = {}): Promise<Response> {
   return app.handle(
     new Request(url(path), {
       method: "POST",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json", ...headers },
       body: JSON.stringify(body),
     })
   );
 }
 
+async function createAgent(body: Record<string, unknown> = {}, token: string | 
null = TOKEN): Promise<Response> {
+  return postJson(`${API}/agents`, { modelType: "m", ...body }, token ? { 
Authorization: `Bearer ${token}` } : {});
+}
+
 async function patchJson(path: string, body: unknown): Promise<Response> {
   return app.handle(
     new Request(url(path), {
@@ -75,8 +95,8 @@ describe(`GET ${API}/healthcheck`, () => {
 });
 
 describe(`POST ${API}/agents`, () => {
-  test("creates an agent with no delegate", async () => {
-    const res = await postJson(`${API}/agents`, { modelType: "test-model", 
name: "Tester" });
+  test("creates an agent for the delegating user", async () => {
+    const res = await createAgent({ modelType: "test-model", name: "Tester" });
     expect(res.status).toBe(200);
 
     const agent = await readJson<{
@@ -90,12 +110,11 @@ describe(`POST ${API}/agents`, () => {
     expect(agent.name).toBe("Tester");
     expect(agent.modelType).toBe("test-model");
     expect(agent.state).toBe("AVAILABLE");
-    expect(agent.delegate).toBeUndefined();
   });
 
   test("auto-numbers agent ids monotonically", async () => {
-    const a = await readJson<{ id: string }>(await postJson(`${API}/agents`, { 
modelType: "m" }));
-    const b = await readJson<{ id: string }>(await postJson(`${API}/agents`, { 
modelType: "m" }));
+    const a = await readJson<{ id: string }>(await createAgent());
+    const b = await readJson<{ id: string }>(await createAgent());
 
     const aNum = Number(a.id.split("-")[1]);
     const bNum = Number(b.id.split("-")[1]);
@@ -103,20 +122,29 @@ describe(`POST ${API}/agents`, () => {
   });
 
   test("rejects invalid token", async () => {
-    const res = await postJson(`${API}/agents`, {
-      modelType: "m",
-      userToken: "obviously-not-a-jwt",
-    });
+    const res = await createAgent({}, "obviously-not-a-jwt");
     expect(res.status).toBe(401);
     const body = await readJson<{ error: string }>(res);
     expect(body.error).toBe("Invalid or expired token");
   });
 
+  test("rejects missing Authorization header", async () => {
+    const res = await createAgent({}, null);
+    expect(res.status).toBe(401);
+    const body = await readJson<{ error: string }>(res);
+    expect(body.error).toBe("Authorization header with a Bearer token is 
required");
+  });
+
+  test("rejects non-Bearer Authorization header", async () => {
+    const res = await postJson(`${API}/agents`, { modelType: "m" }, { 
Authorization: `Basic ${TOKEN}` });
+    expect(res.status).toBe(401);
+    const body = await readJson<{ error: string }>(res);
+    expect(body.error).toBe("Authorization header with a Bearer token is 
required");
+  });
+
   test("rejects missing modelType", async () => {
-    const res = await postJson(`${API}/agents`, { name: "no-model" });
-    // Body schema violation; the exact status depends on the Elysia version 
but
-    // it is always a 4xx or 5xx, never a successful 2xx.
-    expect(res.status).toBeGreaterThanOrEqual(400);
+    const res = await createAgent({ modelType: undefined, name: "no-model" });
+    expect(res.status).toBe(400);
   });
 });
 
@@ -129,8 +157,8 @@ describe(`GET ${API}/agents`, () => {
   });
 
   test("lists every created agent", async () => {
-    await postJson(`${API}/agents`, { modelType: "m", name: "one" });
-    await postJson(`${API}/agents`, { modelType: "m", name: "two" });
+    await createAgent({ name: "one" });
+    await createAgent({ name: "two" });
 
     const res = await getJson(`${API}/agents`);
     const body = await readJson<{ agents: { name: string }[] }>(res);
@@ -141,7 +169,7 @@ describe(`GET ${API}/agents`, () => {
 
 describe(`GET ${API}/agents/:id`, () => {
   test("returns the agent plus its workflow snapshot", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
 
     const res = await getJson(`${API}/agents/${created.id}`);
     expect(res.status).toBe(200);
@@ -161,7 +189,7 @@ describe(`GET ${API}/agents/:id`, () => {
 
 describe(`DELETE ${API}/agents/:id`, () => {
   test("destroys the agent and a follow-up GET returns 404", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
 
     const delRes = await del(`${API}/agents/${created.id}`);
     expect(delRes.status).toBe(200);
@@ -179,21 +207,21 @@ describe(`DELETE ${API}/agents/:id`, () => {
 
 describe("Agent control routes", () => {
   test("POST /:id/stop returns stopping", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
     const res = await postJson(`${API}/agents/${created.id}/stop`, {});
     expect(res.status).toBe(200);
     expect(await readJson<unknown>(res)).toEqual({ status: "stopping" });
   });
 
   test("POST /:id/clear resets history", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
     const res = await postJson(`${API}/agents/${created.id}/clear`, {});
     expect(res.status).toBe(200);
     expect(await readJson<unknown>(res)).toEqual({ status: "cleared" });
   });
 
   test("GET /:id/operator-results returns an empty map on the framework 
build", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
     const res = await getJson(`${API}/agents/${created.id}/operator-results`);
     expect(res.status).toBe(200);
     expect(await readJson<unknown>(res)).toEqual({ results: {} });
@@ -202,7 +230,7 @@ describe("Agent control routes", () => {
 
 describe(`PATCH ${API}/agents/:id/settings`, () => {
   test("updates settings and returns the new values", async () => {
-    const created = await readJson<{ id: string }>(await 
postJson(`${API}/agents`, { modelType: "m" }));
+    const created = await readJson<{ id: string }>(await createAgent());
 
     const res = await patchJson(`${API}/agents/${created.id}/settings`, {
       maxSteps: 7,
diff --git a/agent-service/src/server.ts b/agent-service/src/server.ts
index d5eeae82c9..0da3f69379 100644
--- a/agent-service/src/server.ts
+++ b/agent-service/src/server.ts
@@ -23,7 +23,7 @@ import { createOpenAI } from "@ai-sdk/openai";
 import { TexeraAgent } from "./agent/texera-agent";
 import { getVisibleResultHeaders } from "./agent/tools/tools-utility";
 import { getBackendConfig } from "./api/backend-api";
-import { extractUserFromToken, validateToken } from "./api/auth-api";
+import { extractBearerToken, extractUserFromToken, validateToken } from 
"./api/auth-api";
 import { retrieveWorkflow } from "./api/workflow-api";
 import { WorkflowSystemMetadata } from "./agent/util/workflow-system-metadata";
 import { env } from "./config/env";
@@ -46,15 +46,18 @@ let agentCounter = 0;
 
 async function createAgentInstance(
   modelType: string,
-  customName?: string,
-  delegateConfig?: AgentDelegateConfig
+  delegateConfig: AgentDelegateConfig,
+  customName?: string
 ): Promise<{ agentId: string; agent: TexeraAgent }> {
   const agentId = `agent-${++agentCounter}`;
   const config = getBackendConfig();
 
   const openai = createOpenAI({
     baseURL: `${config.modelsEndpoint}/api`,
-    apiKey: env.LLM_API_KEY,
+    // The LLM gateway (access-control-service) enforces a REGULAR/ADMIN-role
+    // JWT (apache/texera#5421) and injects the LiteLLM master key downstream,
+    // so the delegating user's JWT is the only credential this service sends.
+    apiKey: delegateConfig.userToken,
   });
 
   // Reasoning effort variants are configured as separate model entries in 
litellm-config.yaml
@@ -68,7 +71,7 @@ async function createAgentInstance(
 
   await agent.initialize();
 
-  if (delegateConfig?.workflowId && delegateConfig.userToken) {
+  if (delegateConfig.workflowId) {
     try {
       const workflow = await retrieveWorkflow(delegateConfig.userToken, 
delegateConfig.workflowId);
       delegateConfig.workflowName = workflow.name;
@@ -91,7 +94,7 @@ async function createAgentInstance(
   }
 
   agentStore.set(agentId, agent);
-  log.info({ agentId, delegate: !!delegateConfig }, "created agent");
+  log.info({ agentId, userId: delegateConfig.userInfo?.uid }, "created agent");
 
   return { agentId, agent };
 }
@@ -138,26 +141,27 @@ function getAgent(agentId: string): TexeraAgent {
   return agent;
 }
 
+// Status codes for handler-thrown errors; anything unlisted is a 500.
+const ERROR_STATUS: Record<string, number> = {
+  "Agent not found": 404,
+  "Invalid or expired token": 401,
+  "Authorization header with a Bearer token is required": 401,
+  "modelType is required": 400,
+};
+
 const agentsRouter = new Elysia({ prefix: "/agents" })
   // Error handler must live on the same Elysia instance whose routes throw, or
   // its scope will not see the errors. Elysia 1.x defaults to local scoping 
for
   // .onError, so attach here rather than on the outer app.
-  .onError(({ error, set }) => {
+  .onError(({ code, error, set }) => {
     log.error({ err: error }, "request error");
     const errorMessage = error instanceof Error ? error.message : 
String(error);
-    if (errorMessage === "Agent not found") {
-      set.status = 404;
-      return { error: "Agent not found" };
-    }
-    if (errorMessage === "Invalid or expired token") {
-      set.status = 401;
-      return { error: "Invalid or expired token" };
-    }
-    if (errorMessage === "modelType is required") {
+    // Body schema violations and malformed JSON are client errors, not 500s.
+    if (code === "VALIDATION" || code === "PARSE") {
       set.status = 400;
-      return { error: "modelType is required" };
+      return { error: errorMessage || "Invalid request body" };
     }
-    set.status = 500;
+    set.status = ERROR_STATUS[errorMessage] ?? 500;
     return { error: errorMessage || "Internal server error" };
   })
   .get("/", () => {
@@ -167,29 +171,33 @@ const agentsRouter = new Elysia({ prefix: "/agents" })
 
   .post(
     "/",
-    async ({ body }) => {
-      const { modelType, name, userToken, workflowId, computingUnitId, 
settings } = body as CreateAgentRequest;
+    async ({ body, headers }) => {
+      const { modelType, name, workflowId, computingUnitId, settings } = body 
as CreateAgentRequest;
 
       if (!modelType) {
         throw new Error("modelType is required");
       }
 
-      let delegateConfig: AgentDelegateConfig | undefined;
-      if (userToken) {
-        if (!validateToken(userToken)) {
-          throw new Error("Invalid or expired token");
-        }
-
-        const userInfo = extractUserFromToken(userToken);
-        delegateConfig = {
-          userToken,
-          userInfo,
-          workflowId,
-          computingUnitId,
-        };
+      // The agent always calls the LLM gateway as the delegating user, so an
+      // agent without a user token would be unable to generate anything. The
+      // token travels in the Authorization header, never in the payload.
+      const userToken = extractBearerToken(headers.authorization);
+      if (!userToken) {
+        throw new Error("Authorization header with a Bearer token is 
required");
+      }
+      if (!validateToken(userToken)) {
+        throw new Error("Invalid or expired token");
       }
 
-      const { agentId, agent } = await createAgentInstance(modelType, name, 
delegateConfig);
+      const userInfo = extractUserFromToken(userToken);
+      const delegateConfig: AgentDelegateConfig = {
+        userToken,
+        userInfo,
+        workflowId,
+        computingUnitId,
+      };
+
+      const { agentId, agent } = await createAgentInstance(modelType, 
delegateConfig, name);
 
       if (settings) {
         log.info(
@@ -220,7 +228,6 @@ const agentsRouter = new Elysia({ prefix: "/agents" })
       body: t.Object({
         modelType: t.String(),
         name: t.Optional(t.String()),
-        userToken: t.Optional(t.String()),
         workflowId: t.Optional(t.Number()),
         computingUnitId: t.Optional(t.Number()),
         settings: t.Optional(
@@ -630,7 +637,6 @@ function printStartupMessage(app: ReturnType<typeof 
buildApp>) {
 
   console.log("");
   console.log("Environment:");
-  console.log(`  LLM_API_KEY: ${env.LLM_API_KEY === "dummy" ? "dummy 
(default)" : "set"}`);
   console.log(`  LLM_ENDPOINT: ${getBackendConfig().modelsEndpoint}`);
   console.log(`  WORKFLOW_COMPILING_SERVICE_ENDPOINT: 
${getBackendConfig().compileEndpoint}`);
   console.log(`  TEXERA_DASHBOARD_SERVICE_ENDPOINT: 
${getBackendConfig().apiEndpoint}`);
diff --git a/agent-service/src/types/agent.ts b/agent-service/src/types/agent.ts
index 765f5a7cb4..694b51785f 100644
--- a/agent-service/src/types/agent.ts
+++ b/agent-service/src/types/agent.ts
@@ -147,7 +147,6 @@ export interface AgentInfo {
 export interface CreateAgentRequest {
   modelType: string;
   name?: string;
-  userToken?: string;
   workflowId?: number;
   computingUnitId?: number;
   settings?: AgentSettingsApi;
diff --git a/bin/k8s/templates/agent-service-deployment.yaml 
b/bin/k8s/templates/agent-service-deployment.yaml
index 5437cd7d9c..399b3a9058 100644
--- a/bin/k8s/templates/agent-service-deployment.yaml
+++ b/bin/k8s/templates/agent-service-deployment.yaml
@@ -56,11 +56,6 @@ spec:
             # computing unit id at request time.
             - name: EXECUTION_ENDPOINT_TEMPLATE
               value: http://computing-unit-{cuid}.{{ 
.Values.workflowComputingUnitPool.name }}-svc.{{ 
.Values.workflowComputingUnitPool.namespace }}.svc.cluster.local:{{ 
.Values.workflowComputingUnitPool.service.port }}
-            - name: LLM_API_KEY
-              valueFrom:
-                secretKeyRef:
-                  name: {{ .Release.Name }}-agent-service-secret
-                  key: llm-api-key
           # The service loads operator metadata from the dashboard service on
           # startup, so gate readiness on its health endpoint before the 
gateway
           # routes traffic here. /api/healthcheck needs no auth.
diff --git a/bin/k8s/templates/agent-service-secret.yaml 
b/bin/k8s/templates/agent-service-secret.yaml
index 61746a0aeb..abe7adb2a0 100644
--- a/bin/k8s/templates/agent-service-secret.yaml
+++ b/bin/k8s/templates/agent-service-secret.yaml
@@ -15,10 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Shared secret for the agent service and LiteLLM. Holds the agent's gateway
-# key, LiteLLM's master key, and the upstream provider API keys. Provide real
-# values via `--set` or a values override file; do not commit them.
-{{- if or .Values.agentService.enabled .Values.litellm.enabled }}
+# Secret for the LLM gateway. Holds LiteLLM's master key (consumed by LiteLLM
+# and the access-control-service, never by the agent service, which
+# authenticates with the delegating user's JWT) and the upstream provider API
+# keys. Provide real values via `--set` or a values override file; do not
+# commit them.
+{{- if .Values.litellm.enabled }}
 apiVersion: v1
 kind: Secret
 metadata:
@@ -26,7 +28,6 @@ metadata:
   namespace: {{ .Release.Namespace }}
 type: Opaque
 stringData:
-  llm-api-key: "{{ .Values.agentService.env.llmApiKey }}"
   litellm-master-key: "{{ .Values.litellm.masterKey }}"
   {{- range $key, $value := .Values.litellm.providerApiKeys }}
   {{ $key }}: "{{ $value }}"
diff --git a/bin/k8s/values-development.yaml b/bin/k8s/values-development.yaml
index 5537b39acc..dc7078e468 100644
--- a/bin/k8s/values-development.yaml
+++ b/bin/k8s/values-development.yaml
@@ -233,10 +233,6 @@ agentService:
   service:
     type: ClusterIP
     port: 3001
-  env:
-    # Authenticates the agent service to the in-cluster LLM gateway
-    # (access-control-service / LiteLLM), not to the upstream provider.
-    llmApiKey: "dummy"
 
 litellm:
   enabled: true
diff --git a/bin/k8s/values.yaml b/bin/k8s/values.yaml
index 32687a7ac5..2974c27c88 100644
--- a/bin/k8s/values.yaml
+++ b/bin/k8s/values.yaml
@@ -215,10 +215,6 @@ agentService:
   service:
     type: ClusterIP
     port: 3001
-  env:
-    # Authenticates the agent service to the in-cluster LLM gateway
-    # (access-control-service / LiteLLM), not to the upstream provider.
-    llmApiKey: "dummy"
 
 litellm:
   enabled: true
diff --git a/bin/single-node/.env b/bin/single-node/.env
index 54aa2f5b32..555e14db7d 100644
--- a/bin/single-node/.env
+++ b/bin/single-node/.env
@@ -93,7 +93,6 @@ LITELLM_BASE_URL=http://litellm:4000
 
 # Configurations for agent-service to connect to Texera's services
 LLM_ENDPOINT=http://nginx:8080
-LLM_API_KEY=dummy
 TEXERA_DASHBOARD_SERVICE_ENDPOINT=http://dashboard-service:8080
 WORKFLOW_COMPILING_SERVICE_ENDPOINT=http://workflow-compiling-service:9090
 
WORKFLOW_EXECUTION_SERVICE_ENDPOINT=http://workflow-runtime-coordinator-service:8085
diff --git a/frontend/src/app/workspace/service/agent/agent.service.spec.ts 
b/frontend/src/app/workspace/service/agent/agent.service.spec.ts
new file mode 100644
index 0000000000..cacf82c40d
--- /dev/null
+++ b/frontend/src/app/workspace/service/agent/agent.service.spec.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { TestBed } from "@angular/core/testing";
+import { HttpClientTestingModule, HttpTestingController } from 
"@angular/common/http/testing";
+import { AgentService, AgentInfo } from "./agent.service";
+import { NotificationService } from 
"../../../common/service/notification/notification.service";
+import { WorkflowPersistService } from 
"../../../common/service/workflow-persist/workflow-persist.service";
+import { ComputingUnitStatusService } from 
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
+import { DashboardWorkflowComputingUnit } from 
"../../../common/type/workflow-computing-unit";
+import { commonTestProviders } from "../../../common/testing/test-utils";
+
+describe("AgentService", () => {
+  let service: AgentService;
+  let httpMock: HttpTestingController;
+  let selectedUnit: DashboardWorkflowComputingUnit | null;
+
+  const apiAgent = {
+    id: "agent-1",
+    name: "Bob",
+    modelType: "gpt-5-mini",
+    state: "AVAILABLE",
+    createdAt: "2026-06-11T00:00:00.000Z",
+  };
+
+  beforeEach(() => {
+    selectedUnit = null;
+    TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
+      providers: [
+        AgentService,
+        { provide: NotificationService, useValue: { error: () => {}, success: 
() => {}, info: () => {} } },
+        { provide: WorkflowPersistService, useValue: {} },
+        {
+          provide: ComputingUnitStatusService,
+          useValue: { getSelectedComputingUnitValue: () => selectedUnit },
+        },
+        ...commonTestProviders,
+      ],
+    });
+    service = TestBed.inject(AgentService);
+    httpMock = TestBed.inject(HttpTestingController);
+    // The constructor syncs the local agent cache with the backend.
+    httpMock.expectOne(req => req.method === "GET" && req.url === 
"/api/agents").flush({ agents: [] });
+  });
+
+  afterEach(() => {
+    httpMock.verify();
+  });
+
+  describe("createAgent", () => {
+    it("creates an agent without putting the user token in the payload", () => 
{
+      let created: AgentInfo | undefined;
+      service.createAgent("gpt-5-mini", "Bob").subscribe(agent => (created = 
agent));
+
+      const req = httpMock.expectOne(r => r.method === "POST" && r.url === 
"/api/agents");
+      expect(req.request.body.userToken).toBeUndefined();
+      expect(req.request.body.modelType).toEqual("gpt-5-mini");
+      expect(req.request.body.name).toEqual("Bob");
+      expect(req.request.body.workflowId).toBeUndefined();
+      expect(req.request.body.computingUnitId).toBeUndefined();
+      req.flush(apiAgent);
+
+      expect(created?.id).toEqual("agent-1");
+      expect(created?.modelType).toEqual("gpt-5-mini");
+    });
+
+    it("includes workflowId and the selected computing unit id in the 
payload", () => {
+      selectedUnit = { computingUnit: { cuid: 7 } } as unknown as 
DashboardWorkflowComputingUnit;
+      service.createAgent("gpt-5-mini", "Bob", 42).subscribe();
+
+      const req = httpMock.expectOne(r => r.method === "POST" && r.url === 
"/api/agents");
+      expect(req.request.body.workflowId).toEqual(42);
+      expect(req.request.body.computingUnitId).toEqual(7);
+      expect(req.request.body.userToken).toBeUndefined();
+      req.flush(apiAgent);
+    });
+  });
+});
diff --git a/frontend/src/app/workspace/service/agent/agent.service.ts 
b/frontend/src/app/workspace/service/agent/agent.service.ts
index 2009734030..462e7679ce 100644
--- a/frontend/src/app/workspace/service/agent/agent.service.ts
+++ b/frontend/src/app/workspace/service/agent/agent.service.ts
@@ -37,7 +37,6 @@ import {
 import { NotificationService } from 
"../../../common/service/notification/notification.service";
 import { WorkflowPersistService } from 
"../../../common/service/workflow-persist/workflow-persist.service";
 import { AppSettings } from "../../../common/app-setting";
-import { AuthService } from "../../../common/service/user/auth.service";
 import { AgentState, ReActStep, ModelMessage } from "./agent-types";
 import { Workflow, WorkflowContent } from "../../../common/type/workflow";
 import { ComputingUnitStatusService } from 
"../../../common/service/computing-unit/computing-unit-status/computing-unit-status.service";
@@ -685,31 +684,26 @@ export class AgentService {
 
   /**
    * Create a new agent with the specified model type.
-   * Uses the user's current auth token for delegate mode.
+   * The user's JWT travels in the Authorization header (added by the JWT
+   * interceptor), which the agent service requires for delegate mode.
    * @param modelType - The LLM model type to use
    * @param customName - Optional custom name for the agent
    * @param workflowId - Optional workflow ID for delegate mode
    */
   public createAgent(modelType: string, customName?: string, workflowId?: 
number): Observable<AgentInfo> {
     return defer(() => {
-      const userToken = AuthService.getAccessToken();
-
       const body: any = {
         modelType,
         name: customName,
       };
 
-      // Include user token and workflowId for delegate mode if available
-      if (userToken) {
-        body.userToken = userToken;
-        if (workflowId !== undefined) {
-          body.workflowId = workflowId;
-        }
-        // Include computing unit ID for workflow execution
-        const selectedUnit = 
this.computingUnitStatusService.getSelectedComputingUnitValue();
-        if (selectedUnit) {
-          body.computingUnitId = selectedUnit.computingUnit.cuid;
-        }
+      if (workflowId !== undefined) {
+        body.workflowId = workflowId;
+      }
+      // Include computing unit ID for workflow execution
+      const selectedUnit = 
this.computingUnitStatusService.getSelectedComputingUnitValue();
+      if (selectedUnit) {
+        body.computingUnitId = selectedUnit.computingUnit.cuid;
       }
 
       return this.http.post<ApiAgentInfo>(`${this.AGENT_API_BASE}/agents`, 
body).pipe(

Reply via email to