Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion sdks/python/agenta/sdk/middlewares/running/vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@
if "mistral" not in _PROVIDER_KINDS:
_PROVIDER_KINDS.append("mistral")

# Mapping from provider kind to environment variable name.
# Most providers follow the pattern PROVIDER_API_KEY, but some have
# underscores in their kind string (e.g. "together_ai") where the env
# var drops the underscore (TOGETHERAI_API_KEY). This explicit mapping
# mirrors the one in the Daytona runner and the frontend llmProviders.ts.
_PROVIDER_ENV_VAR_MAP: Dict[str, str] = {
"openai": "OPENAI_API_KEY",
"cohere": "COHERE_API_KEY",
"anyscale": "ANYSCALE_API_KEY",
"deepinfra": "DEEPINFRA_API_KEY",
"alephalpha": "ALEPHALPHA_API_KEY",
"groq": "GROQ_API_KEY",
"minimax": "MINIMAX_API_KEY",
"mistral": "MISTRAL_API_KEY",
"mistralai": "MISTRAL_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"perplexityai": "PERPLEXITYAI_API_KEY",
"together_ai": "TOGETHERAI_API_KEY",
"openrouter": "OPENROUTER_API_KEY",
"gemini": "GEMINI_API_KEY",
}

_AUTH_ENABLED = (
getenv("AGENTA_SERVICES_MIDDLEWARE_AUTH_ENABLED")
or getenv("AGENTA_SERVICE_MIDDLEWARE_AUTH_ENABLED")
Expand Down Expand Up @@ -306,7 +328,7 @@ async def get_secrets(
try:
for provider_kind in _PROVIDER_KINDS:
provider = provider_kind
key_name = f"{provider.upper()}_API_KEY"
key_name = _PROVIDER_ENV_VAR_MAP.get(provider, f"{provider.upper()}_API_KEY")
key = getenv(key_name)

if not key:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ export const triggerRunInvocationAtom = atom(
traceId: result.traceId ?? undefined,
status: "failure",
references,
error: {message: errorMessage},
error: {
message: errorMessage,
...(result.error?.stacktrace ? {stacktrace: result.error.stacktrace} : {}),
...(result.error?.type ? {type: result.error.type} : {}),
},
})

await updateScenarioStatus(scenarioId, EvaluationStatus.FAILURE)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type {MouseEvent} from "react"

import {message} from "@agenta/ui/app-message"
import {getDefaultStore} from "jotai"
import Router from "next/router"
Expand All @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState

const getActiveAppId = (): string | null => store.get(routerAppIdAtom)

export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement | null
if (!target) return false
const interactiveSelector =
"button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger"
return Boolean(target.closest(interactiveSelector))
}

interface NavigateToRunParams {
record: EvaluationRunTableRow
scope: "app" | "project"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {activePreviewProjectIdAtom} from "@/oss/components/EvalRunDetails/atoms/
import {clearAllMetricStatsCaches} from "@/oss/components/EvalRunDetails/atoms/runMetrics"
import {
InfiniteVirtualTableFeatureShell,
shouldIgnoreRowClick,
type TableFeaturePagination,
type TableScopeConfig,
} from "@/oss/components/InfiniteVirtualTable"
Expand All @@ -34,7 +35,6 @@ import {
} from "@/oss/lib/onboarding"
import {useQueryParamState} from "@/oss/state/appState"

import {shouldIgnoreRowClick} from "../../actions/navigationActions"
import {
evaluationRunsDeleteContextAtom,
evaluationRunsTableFetchEnabledAtom,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,36 @@ import useTableExport from "./useTableExport"
const dummySearchAtom = atom("")

/**
* Helper to detect if a click event should be ignored for row navigation
* Default CSS selectors for interactive elements that should not trigger row navigation.
* Consolidated from all table implementations to ensure consistent click-through behavior.
*/
export const INTERACTIVE_ROW_SELECTORS = [
"button",
"a",
"input",
"textarea",
"select",
"[role='button']",
"[role='menuitem']",
"[role='checkbox']",
"[data-interactive]",
".ant-dropdown-trigger",
".ant-checkbox-wrapper",
".ant-checkbox",
".ant-checkbox-input",
".ant-checkbox-inner",
".ant-btn",
".ant-select",
].join(", ")

/**
* Helper to detect if a click event should be ignored for row navigation.
* Returns true if the click was on an interactive element (button, link, dropdown, etc.)
*/
export const shouldIgnoreRowClick = (event: MouseEvent<HTMLElement>): boolean => {
const target = event.target as HTMLElement

// Check if clicking on interactive elements
if (
target.closest("button") ||
target.closest("a") ||
target.closest(".ant-dropdown-trigger") ||
target.closest(".ant-checkbox-wrapper") ||
target.closest(".ant-select") ||
target.closest("input") ||
target.closest("textarea")
) {
return true
}

return false
if (!target) return false
return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS))
}

/** Configuration for built-in search. When provided, the hook manages search state internally. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const EvaluatorDetailsPopover = ({
navigateToEvaluator(evaluator)
}}
>
Open evaluator registry
Open evaluator playground
</Button>
) : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,9 @@ const useEvaluatorNavigation = () => {
const identifier = getEvaluatorIdentifier(evaluator)
if (!identifier) return null

if (isHumanEvaluator(evaluator)) {
return {
href: `${projectURL}/evaluators?tab=human&openEvaluator=${encodeURIComponent(
identifier,
)}`,
type: "human",
}
}

return {
href: `${projectURL}/evaluators/playground?revisions=${encodeURIComponent(identifier)}`,
type: "auto",
type: isHumanEvaluator(evaluator) ? "human" : "auto",
}
},
[projectURL],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {useCallback, useMemo, useState} from "react"
import React, {useCallback, useMemo, useState} from "react"

import {
ColumnVisibilityMenuTrigger,
defaultHeaderVariant,
detectColumnTypes,
InfiniteVirtualTableFeatureShell,
shouldIgnoreRowClick,
type TableScopeConfig,
type TypeChipConfig,
useTypeChipFeature,
Expand Down Expand Up @@ -758,7 +759,10 @@ export function TestcasesTableShell(props: TestcasesTableShellProps) {
size: "small" as const,
bordered: true,
onRow: (record: TestcaseTableRow) => ({
onClick: () => onRowClick(record),
onClick: (event: React.MouseEvent) => {
if (shouldIgnoreRowClick(event)) return
onRowClick(record)
},
className: "cursor-pointer hover:bg-gray-50",
}),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {type Key, type ReactNode, useCallback, useEffect, useMemo, useState} from "react"

import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table"
import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table"
import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table"
import {useAtomValue, useSetAtom, useStore} from "jotai"
import dynamic from "next/dynamic"
Expand Down Expand Up @@ -307,7 +307,10 @@ const ObservabilityTable = () => {
sticky: true,
style: {cursor: "pointer"},
onRow: (record, index) => ({
onClick: () => handleTraceRowClick(record),
onClick: (event) => {
if (shouldIgnoreRowClick(event)) return
handleTraceRowClick(record)
},
"data-tour": index === 0 ? "trace-row" : undefined,
}),
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useCallback, useEffect, useMemo, useState} from "react"

import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table"
import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table"
import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table"
import {useAtomValue, useSetAtom} from "jotai"
import dynamic from "next/dynamic"
Expand Down Expand Up @@ -141,7 +141,10 @@ const SessionsTable: React.FC = () => {
bordered: true,
loading: isLoading && sessionIds.length === 0,
onRow: (record) => ({
onClick: () => openDrawer({sessionId: record.session_id}),
onClick: (event) => {
if (shouldIgnoreRowClick(event)) return
openDrawer({sessionId: record.session_id})
},
style: {cursor: "pointer"},
}),
}}
Expand Down
6 changes: 5 additions & 1 deletion web/oss/src/components/pages/prompts/PromptsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TableFeaturePagination,
TableScopeConfig,
} from "@agenta/ui/table"
import {shouldIgnoreRowClick} from "@agenta/ui/table"
import {message} from "antd"
import type {TableProps} from "antd/es/table"
import {useAtomValue, useSetAtom} from "jotai"
Expand Down Expand Up @@ -686,7 +687,10 @@ const PromptsPage = () => {
scroll: {x: "max-content" as const},
expandable: tableExpandableConfig,
onRow: (record: PromptsTableRow) => ({
onClick: () => handleRowClick(record),
onClick: (event: React.MouseEvent) => {
if (shouldIgnoreRowClick(event)) return
handleRowClick(record)
},
className: "cursor-pointer",
draggable: true,
onDragStart: (event: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useState} from "react"
import type {User} from "@agenta/shared/types"
import {message} from "@agenta/ui/app-message"
import {EditOutlined, MoreOutlined, SyncOutlined} from "@ant-design/icons"
import {ArrowClockwise, Trash} from "@phosphor-icons/react"
import {ArrowClockwise, Key, Trash} from "@phosphor-icons/react"
import {Button, Dropdown, Input, Modal, Space, Tag, Tooltip, Typography} from "antd"

import AlertPopup from "@/oss/components/AlertPopup/AlertPopup"
Expand All @@ -12,7 +12,7 @@ import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE"
import {useEntitlements} from "@/oss/lib/helpers/useEntitlements"
import {snakeToTitle} from "@/oss/lib/helpers/utils"
import {WorkspaceMember} from "@/oss/lib/Types"
import {updateUsername} from "@/oss/services/profile"
import {resetPassword, updateUsername} from "@/oss/services/profile"
import {
assignWorkspaceRole,
removeFromWorkspace,
Expand All @@ -23,6 +23,9 @@ import {useOrgData} from "@/oss/state/org"
import {useProfileData} from "@/oss/state/profile"
import {useWorkspaceRoles} from "@/oss/state/workspace"

import GenerateResetLinkModal from "./Modals/GenerateResetLinkModal"
import PasswordResetLinkModal from "./Modals/PasswordResetLinkModal"

export const Actions: React.FC<{
member: WorkspaceMember
hidden?: boolean
Expand All @@ -39,6 +42,10 @@ export const Actions: React.FC<{
const {refetch: refetchProfile} = useProfileData()
const [renameOpen, setRenameOpen] = useState(false)
const [renameValue, setRenameValue] = useState(user.username || "")
const [generateResetLinkOpen, setGenerateResetLinkOpen] = useState(false)
const [resetLinkOpen, setResetLinkOpen] = useState(false)
const [resetLink, setResetLink] = useState("")
const [resetLoading, setResetLoading] = useState(false)

if (hidden && !selfMenu) return null

Expand Down Expand Up @@ -90,6 +97,24 @@ export const Actions: React.FC<{
}
}

const handleResetPassword = async () => {
setResetLoading(true)
try {
const link = await resetPassword(user.id)
setGenerateResetLinkOpen(false)
setResetLink(link)
setResetLinkOpen(true)
} catch (error: any) {
const detail =
error?.response?.data?.detail ||
error?.message ||
"Unable to generate reset password link"
message.error(detail)
} finally {
setResetLoading(false)
}
}

return (
<>
<Dropdown
Expand Down Expand Up @@ -127,6 +152,19 @@ export const Actions: React.FC<{
},
]
: []),
...(isMember
? [
{
key: "reset_password",
label: "Reset password",
icon: <Key size={16} />,
onClick: (e: any) => {
e.domEvent.stopPropagation()
setGenerateResetLinkOpen(true)
},
},
]
: []),
{
key: "remove",
label: "Remove",
Expand Down Expand Up @@ -165,6 +203,21 @@ export const Actions: React.FC<{
placeholder="New username"
/>
</Modal>

<GenerateResetLinkModal
open={generateResetLinkOpen}
username={user.username}
onCancel={() => setGenerateResetLinkOpen(false)}
onOk={handleResetPassword}
confirmLoading={resetLoading}
/>
Comment on lines +207 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Generate-link modal closes before async completion

Line 211 wires async generation correctly, but the modal currently calls onOk and then immediately onCancel in GenerateResetLinkModal (web/oss/src/components/pages/settings/WorkspaceManage/Modals/GenerateResetLinkModal.tsx:7-12). That makes confirmLoading at Line 212 ineffective and forces users to reopen the modal after failures.

Suggested fix (in GenerateResetLinkModal.tsx)
-const onGenerateLink = () => {
-    props.onOk?.({} as any)
-    props.onCancel?.({} as any)
-}
+const onGenerateLink = async () => {
+    try {
+        await props.onOk?.({} as any)
+        props.onCancel?.({} as any)
+    } catch {
+        // keep modal open so user can retry
+    }
+}


<PasswordResetLinkModal
open={resetLinkOpen}
username={user.username}
generatedLink={resetLink}
onCancel={() => setResetLinkOpen(false)}
/>
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion web/oss/src/services/evaluations/invocations/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const upsertStepResultWithInvocation = async ({
status: string
references?: InvocationReferences
outputs?: unknown
error?: {message: string; stacktrace?: string}
error?: {message: string; stacktrace?: string; type?: string}
}): Promise<void> => {
const {projectId} = getProjectValues()

Expand Down
14 changes: 14 additions & 0 deletions web/oss/src/services/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,17 @@ export const changePassword = async (payload: {
body: JSON.stringify(payload),
})
}

/**
* Generate a password reset link for a user (admin action).
* Returns the reset password link string.
*/
export const resetPassword = async (userId: string): Promise<string> => {
const base = getBaseUrl()
const url = new URL("api/profile/reset-password", base)
url.searchParams.set("user_id", userId)
const data = await fetchJson<string>(url, {
method: "POST",
})
Comment on lines +64 to +70
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Route this new API call through the Fern SDK client

Line 64 introduces a new frontend API call, but it bypasses the Fern-generated client and manual URL/query handling is used instead. Please switch this to getAgentaSdkClient({host: getAgentaApiUrl()}), pass user_id via queryParams, and validate the response at the boundary with safeParseWithLogging.

Suggested direction
-export const resetPassword = async (userId: string): Promise<string> => {
-    const base = getBaseUrl()
-    const url = new URL("api/profile/reset-password", base)
-    url.searchParams.set("user_id", userId)
-    const data = await fetchJson<string>(url, {
-        method: "POST",
-    })
-    return data
-}
+export const resetPassword = async (userId: string): Promise<string> => {
+    const client = getAgentaSdkClient({host: getAgentaApiUrl()})
+    const raw = await client.users.resetUserPassword(
+        {user_id: userId},
+        {queryParams: {user_id: userId}},
+    )
+    const parsed = safeParseWithLogging(/* zod string schema */, raw, "resetPassword")
+    if (!parsed.success) throw new Error("Invalid reset-password response")
+    return parsed.data
+}

As per coding guidelines: “All new frontend API code must go through the Fern-generated client, not raw axios”, “Use getAgentaSdkClient({host: getAgentaApiUrl()})”, “Pass query params via {queryParams: {...}}”, and “Keep zod validation at the boundary of API calls using safeParseWithLogging”.

Source: Coding guidelines

return data
}
Comment on lines +64 to +72
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Promise<string> overstates the reset-password response contract

Line 64 assumes a guaranteed string return, but upstream service behavior can return no link when Sendgrid is configured (api/oss/src/services/user_service.py:130-170). This makes the current type/flow unsafe and can surface an empty/invalid link downstream.

Please align contract explicitly: either make backend always return a link string, or change frontend type/UX to handle “email sent, no link returned” as a separate success path.

Loading