diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.test.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.test.tsx
new file mode 100644
index 0000000000..b25323e0d5
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.test.tsx
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { humanizeKey } from '@studio/routes/agents/AgentDetailRoute/configFormat';
+import { ConfigValue } from '@studio/routes/agents/AgentDetailRoute/ConfigValue';
+import { render, screen } from '@studio/tests/util/render';
+
+describe('humanizeKey', () => {
+ it.each([
+ ['_type', 'Type'],
+ ['model_name', 'Model Name'],
+ ['llm_name', 'LLM Name'],
+ ['base_url', 'Base URL'],
+ ['parseAgentResponseMaxRetries', 'Parse Agent Response Max Retries'],
+ ])('humanizes %s -> %s', (input, expected) => {
+ expect(humanizeKey(input)).toBe(expected);
+ });
+});
+
+describe('ConfigValue', () => {
+ it('renders a scalar as a single row', () => {
+ render();
+ expect(screen.getByText('Verbose')).toBeInTheDocument();
+ expect(screen.getByText('false')).toBeInTheDocument();
+ });
+
+ it('joins a scalar array', () => {
+ render();
+ expect(screen.getByText('Tool Names')).toBeInTheDocument();
+ expect(screen.getByText('wiki, clock')).toBeInTheDocument();
+ });
+
+ it('masks sensitive keys', () => {
+ render();
+ expect(screen.queryByText('super-secret')).not.toBeInTheDocument();
+ expect(screen.getByText('••••••••')).toBeInTheDocument();
+ });
+
+ it.each(['max_tokens', 'maxTokens'])('does not mask token-count key %s', (label) => {
+ render();
+ expect(screen.getByText('4096')).toBeInTheDocument();
+ expect(screen.queryByText('••••••••')).not.toBeInTheDocument();
+ });
+
+ it('recurses into nested objects', () => {
+ render();
+ expect(screen.getByText('Type')).toBeInTheDocument();
+ expect(screen.getByText('openai')).toBeInTheDocument();
+ expect(screen.getByText('Temperature')).toBeInTheDocument();
+ });
+});
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.tsx
new file mode 100644
index 0000000000..cf3cc44148
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigValue.tsx
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { KVPair } from '@nemo/common/src/components/KVPair';
+import { Stack, Text } from '@nvidia/foundations-react-core';
+import { humanizeKey } from '@studio/routes/agents/AgentDetailRoute/configFormat';
+import type { FC, ReactNode } from 'react';
+
+const SENSITIVE_KEY = /(api[_-]?key|secret|token|password|passwd|credential)/i;
+const MASK = '••••••••';
+
+/** Lowercase, separator-stripped so snake_case and camelCase keys compare equal. */
+const normalizeKey = (key: string): string => key.toLowerCase().replace(/[_-]/g, '');
+
+/** Keys that match SENSITIVE_KEY on substring but are ordinary config, not secrets. */
+const NON_SENSITIVE_KEYS = new Set(
+ [
+ 'max_tokens',
+ 'max_new_tokens',
+ 'max_input_tokens',
+ 'max_output_tokens',
+ 'max_completion_tokens',
+ 'max_prompt_tokens',
+ 'num_tokens',
+ 'token_limit',
+ 'context_window_tokens',
+ ].map(normalizeKey)
+);
+
+const isSensitiveKey = (key: string): boolean =>
+ !NON_SENSITIVE_KEYS.has(normalizeKey(key)) && SENSITIVE_KEY.test(key);
+
+const isScalar = (value: unknown): boolean =>
+ value === null || ['string', 'number', 'boolean'].includes(typeof value);
+
+const formatScalar = (value: unknown): string => {
+ if (value === null || value === undefined) return '—';
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
+ return String(value);
+};
+
+interface ConfigValueProps {
+ label: string;
+ value: unknown;
+}
+
+/**
+ * Recursively renders an arbitrary config value as KVPair rows. Scalars (and
+ * scalar-only arrays) collapse to a single row; nested objects/arrays indent.
+ * Values under sensitive keys (api_key, token, secret, …) are masked.
+ */
+export const ConfigValue: FC = ({ label, value }) => {
+ const heading = humanizeKey(label);
+
+ if (isSensitiveKey(label) && value != null && value !== '') {
+ return ;
+ }
+
+ if (Array.isArray(value)) {
+ if (value.length === 0) return ;
+ if (value.every(isScalar)) {
+ return ;
+ }
+ return (
+
+ {value.map((item, index) => (
+
+ ))}
+
+ );
+ }
+
+ if (value !== null && typeof value === 'object') {
+ const entries = Object.entries(value as Record);
+ if (entries.length === 0) return ;
+ return (
+
+ {entries.map(([childKey, childValue]) => (
+
+ ))}
+
+ );
+ }
+
+ return ;
+};
+
+const NestedBlock: FC<{ heading: string; children: ReactNode }> = ({ heading, children }) => (
+
+
+ {heading}
+
+
+ {children}
+
+
+);
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigurationTab.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigurationTab.tsx
deleted file mode 100644
index 10acf6723b..0000000000
--- a/web/packages/studio/src/routes/agents/AgentDetailRoute/ConfigurationTab.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-import { KVPair } from '@nemo/common/src/components/KVPair';
-import { isDefined } from '@nemo/common/src/utils/list';
-import type { Agent } from '@nemo/sdk/generated/agents/schema/Agent';
-import { Stack } from '@nvidia/foundations-react-core';
-import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
-import { getAgentModelNames } from '@studio/components/dataViews/AgentsDataView/utils';
-import { DetailPanel } from '@studio/routes/agents/AgentDetailRoute/overview/DetailPanel';
-import type { FC } from 'react';
-
-interface ConfigurationTabProps {
- workspace: string;
- agentName?: string;
- agent?: Agent;
-}
-
-/** Read-only view of the agent's configuration metadata. */
-export const ConfigurationTab: FC = ({ workspace, agentName, agent }) => {
- const models = getAgentModelNames(agent?.config as AgentConfig | undefined);
-
- return (
-
-
-
-
- {isDefined(agent?.description) && (
-
- )}
- {models.length > 0 && }
- {isDefined(agent?.config_format) && (
-
- )}
-
-
- );
-};
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/DetailsTab.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/DetailsTab.tsx
new file mode 100644
index 0000000000..3ede8f5c88
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/DetailsTab.tsx
@@ -0,0 +1,116 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { KVPair } from '@nemo/common/src/components/KVPair';
+import { RelativeTime } from '@nemo/common/src/components/RelativeTime';
+import { isDefined } from '@nemo/common/src/utils/list';
+import type { Agent } from '@nemo/sdk/generated/agents/schema/Agent';
+import { Stack, Text } from '@nvidia/foundations-react-core';
+import type { AgentConfig } from '@studio/components/dataViews/AgentsDataView';
+import { getAgentModelNames } from '@studio/components/dataViews/AgentsDataView/utils';
+import { ConfigValue } from '@studio/routes/agents/AgentDetailRoute/ConfigValue';
+import { DetailPanel } from '@studio/routes/agents/AgentDetailRoute/overview/DetailPanel';
+import type { FC } from 'react';
+
+/** Config keys rendered by dedicated structured panels below. */
+const STRUCTURED_KEYS = ['workflow', 'llms', 'functions'] as const;
+
+interface DetailsTabProps {
+ workspace: string;
+ agentName?: string;
+ agent?: Agent;
+}
+
+/**
+ * Read-only view of the exact spec an agent is built with: identity metadata
+ * plus every field of its config, grouped into structured panels with an
+ * "Additional configuration" fallback so nothing in the spec is hidden.
+ */
+export const DetailsTab: FC = ({ workspace, agentName, agent }) => {
+ const config = (agent?.config ?? {}) as Record;
+ const models = getAgentModelNames(agent?.config as AgentConfig | undefined);
+
+ const workflow = asRecord(config['workflow']);
+ const llms = asRecord(config['llms']);
+ const functions = asRecord(config['functions']);
+ const extraEntries = Object.entries(config).filter(
+ ([key]) => !STRUCTURED_KEYS.includes(key as (typeof STRUCTURED_KEYS)[number])
+ );
+
+ return (
+
+
+
+
+
+ {isDefined(agent?.project) && agent.project && (
+
+ )}
+ {isDefined(agent?.description) && agent.description && (
+
+ )}
+ {models.length > 0 && }
+ {isDefined(agent?.config_format) && (
+
+ )}
+ {isDefined(agent?.id) && }
+ {isDefined(agent?.created_at) && agent.created_at && (
+ } />
+ )}
+ {isDefined(agent?.updated_at) && agent.updated_at && (
+ } />
+ )}
+
+
+
+ {workflow && (
+
+
+
+ )}
+
+ {llms && (
+
+
+
+ )}
+
+ {functions && (
+
+
+
+ )}
+
+ {extraEntries.length > 0 && (
+
+
+ {extraEntries.map(([key, value]) => (
+
+ ))}
+
+
+ )}
+
+ {Object.keys(config).length === 0 && (
+
+
+ This agent has no stored configuration.
+
+
+ )}
+
+ );
+};
+
+const asRecord = (value: unknown): Record | undefined =>
+ value !== null && typeof value === 'object' && !Array.isArray(value)
+ ? (value as Record)
+ : undefined;
+
+const ConfigEntries: FC<{ data: Record }> = ({ data }) => (
+
+ {Object.entries(data).map(([key, value]) => (
+
+ ))}
+
+);
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/configFormat.ts b/web/packages/studio/src/routes/agents/AgentDetailRoute/configFormat.ts
new file mode 100644
index 0000000000..75eaa44812
--- /dev/null
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/configFormat.ts
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+const ACRONYMS: Record = {
+ llm: 'LLM',
+ llms: 'LLMs',
+ url: 'URL',
+ api: 'API',
+ id: 'ID',
+ uri: 'URI',
+};
+
+/** Turns a snake_case / camelCase config key into a readable label. */
+export const humanizeKey = (key: string): string =>
+ key
+ .replace(/([a-z\d])([A-Z])/g, '$1 $2')
+ .replace(/[_-]+/g, ' ')
+ .trim()
+ .split(' ')
+ .filter(Boolean)
+ .map((word) => ACRONYMS[word.toLowerCase()] ?? word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/index.test.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/index.test.tsx
index 98de2dfafa..77d07534ca 100644
--- a/web/packages/studio/src/routes/agents/AgentDetailRoute/index.test.tsx
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/index.test.tsx
@@ -29,6 +29,7 @@ describe('AgentDetailRoute', () => {
expect(screen.getByRole('tab', { name: 'Evaluations' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Logs' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Chat' })).toBeInTheDocument();
+ expect(screen.getByRole('tab', { name: 'Details' })).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Overview' })).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Configuration' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open traces' })).toBeInTheDocument();
@@ -46,4 +47,24 @@ describe('AgentDetailRoute', () => {
expect(screen.getByRole('tab', { name: 'Chat' })).toHaveAttribute('aria-selected', 'true');
expect(await screen.findByRole('textbox', { name: /Task prompt/i })).toBeInTheDocument();
});
+
+ it('shows the agent spec on the details tab and masks secrets', async () => {
+ const user = userEvent.setup();
+ renderDetail();
+
+ await user.click(await screen.findByRole('tab', { name: 'Details' }));
+
+ expect(screen.getByRole('tab', { name: 'Details' })).toHaveAttribute('aria-selected', 'true');
+ // Structured panels
+ expect(await screen.findByText('Overview')).toBeInTheDocument();
+ expect(screen.getByText('Workflow')).toBeInTheDocument();
+ expect(screen.getByText('Models')).toBeInTheDocument();
+ expect(screen.getByText('Tools')).toBeInTheDocument();
+ // Config values surfaced from the spec
+ expect(screen.getByText('nat-workflow-v1')).toBeInTheDocument();
+ expect(screen.getByText('react_agent')).toBeInTheDocument();
+ // The llm api_key is masked, never shown raw
+ expect(screen.queryByText('not-used')).not.toBeInTheDocument();
+ expect(screen.getByText('••••••••')).toBeInTheDocument();
+ });
});
diff --git a/web/packages/studio/src/routes/agents/AgentDetailRoute/index.tsx b/web/packages/studio/src/routes/agents/AgentDetailRoute/index.tsx
index c08eae2235..4db08f784d 100644
--- a/web/packages/studio/src/routes/agents/AgentDetailRoute/index.tsx
+++ b/web/packages/studio/src/routes/agents/AgentDetailRoute/index.tsx
@@ -25,6 +25,7 @@ import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs';
import { CreateDeploymentModal } from '@studio/routes/agents/AgentDeploymentsListRoute/CreateDeploymentModal';
import { DeploymentsTab } from '@studio/routes/agents/AgentDetailRoute/DeploymentsTab';
+import { DetailsTab } from '@studio/routes/agents/AgentDetailRoute/DetailsTab';
import { EvaluationsTab } from '@studio/routes/agents/AgentDetailRoute/EvaluationsTab';
import { SubmitEvaluationModal } from '@studio/routes/agents/AgentEvaluationsRoute/components/SubmitEvaluationModal';
import { getAgentMonitorRoute, getAgentsListRoute } from '@studio/routes/utils';
@@ -33,7 +34,7 @@ import { type FC, useRef, useState } from 'react';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
const TAB_SEARCH_PARAM = 'tab';
-const DETAIL_TABS = ['deployments', 'logs', 'chat', 'evaluations'] as const;
+const DETAIL_TABS = ['deployments', 'logs', 'chat', 'evaluations', 'details'] as const;
const DEFAULT_TAB = 'deployments';
type AgentDetailTab = (typeof DETAIL_TABS)[number];
@@ -169,6 +170,7 @@ export const AgentDetailRoute: FC = () => {
Logs
Chat
Evaluations
+ Details
@@ -216,6 +218,10 @@ export const AgentDetailRoute: FC = () => {
/>
+
+
+
+