mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 09:32:24 +00:00
🪪 feat: Add Per-Agent Git Identity (#15628)
* feat: Add per-agent Git identity * fix: annotate git identity schema * fix: complete git identity controls * fix: clear git identity on version restore * fix: Require a complete agent Git identity * fix: Complete Git Identity Across Bash And Form Validation * fix: Discard Incomplete Hidden Git Identity Edits * fix: validate Git identity across panel lifecycle * fix: preserve saved Git identity when discarding drafts
This commit is contained in:
parent
c9b9644163
commit
aeac7a2065
24 changed files with 567 additions and 10 deletions
|
|
@ -1060,6 +1060,7 @@ const updateAgentHandler = async (req, res) => {
|
|||
const {
|
||||
avatar: avatarField,
|
||||
code_environment_id: codeEnvironmentIdField,
|
||||
git_identity: gitIdentityField,
|
||||
_id,
|
||||
...rest
|
||||
} = validatedData;
|
||||
|
|
@ -1067,6 +1068,9 @@ const updateAgentHandler = async (req, res) => {
|
|||
if (codeEnvironmentIdField !== undefined) {
|
||||
updateData.code_environment_id = codeEnvironmentIdField;
|
||||
}
|
||||
if (gitIdentityField !== undefined) {
|
||||
updateData.git_identity = gitIdentityField;
|
||||
}
|
||||
let existingAgent;
|
||||
|
||||
const includesStatefulConfiguration =
|
||||
|
|
@ -1310,6 +1314,10 @@ const updateAgentHandler = async (req, res) => {
|
|||
delete updateData.code_environment_id;
|
||||
updateData.$unset = { code_environment_id: 1 };
|
||||
}
|
||||
if (updateData.git_identity === null) {
|
||||
delete updateData.git_identity;
|
||||
updateData.$unset = { ...updateData.$unset, git_identity: 1 };
|
||||
}
|
||||
|
||||
let updatedAgent =
|
||||
Object.keys(updateData).length > 0
|
||||
|
|
|
|||
|
|
@ -1675,6 +1675,21 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(agentInDb.code_environment_id).toBeUndefined();
|
||||
});
|
||||
|
||||
test('clears a configured Git identity', async () => {
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
{ git_identity: { name: 'Coding Agent', email: 'agent@example.com' } },
|
||||
);
|
||||
mockReq.user.id = existingAgentAuthorId.toString();
|
||||
mockReq.params.id = existingAgentId;
|
||||
mockReq.body = { git_identity: null };
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId });
|
||||
expect(agentInDb.git_identity).toBeUndefined();
|
||||
});
|
||||
|
||||
test('allows unrelated edits to an existing scope after policy is tightened', async () => {
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const {
|
|||
isFatalAgentInitializationError,
|
||||
codeExecutionAuthHeaders,
|
||||
createAttachedWorkspaceBashTool,
|
||||
createGitIdentityProgrammaticBashTool,
|
||||
resolveCodeExecutionContext,
|
||||
resolveCallerCapabilityProjectionSnapshot,
|
||||
LIST_WORKSPACE_FILES_TOOL_NAME,
|
||||
|
|
@ -2093,7 +2094,7 @@ async function loadToolsForExecution({
|
|||
* library so PTC calls share the same managed auth context.
|
||||
*/
|
||||
for (const name of ptcToolNames) {
|
||||
const ptcTool = createBashProgrammaticToolCallingTool({
|
||||
const ptcOptions = {
|
||||
authHeaders: () =>
|
||||
codeExecutionAuthHeaders(
|
||||
(bridgeWorkerId) => getCodeApiAuthHeaders(req, bridgeWorkerId),
|
||||
|
|
@ -2102,7 +2103,11 @@ async function loadToolsForExecution({
|
|||
baseUrl: codeExecutionContext.baseUrl,
|
||||
executionProfile: codeExecutionContext.executionProfile,
|
||||
runtimeSessionHint: codeExecutionContext.runtimeSessionHint,
|
||||
});
|
||||
};
|
||||
const ptcTool =
|
||||
codeExecutionContext.environmentType === 'attached'
|
||||
? createGitIdentityProgrammaticBashTool(ptcOptions, agent?.git_identity)
|
||||
: createBashProgrammaticToolCallingTool(ptcOptions);
|
||||
ptcTool.name = name;
|
||||
allLoadedTools.push(ptcTool);
|
||||
}
|
||||
|
|
@ -2133,6 +2138,7 @@ async function loadToolsForExecution({
|
|||
? createAttachedWorkspaceBashTool({
|
||||
authHeaders,
|
||||
baseUrl: codeExecutionContext.baseUrl,
|
||||
gitIdentity: agent?.git_identity,
|
||||
})
|
||||
: createBashExecutionTool({
|
||||
authHeaders,
|
||||
|
|
|
|||
|
|
@ -2661,6 +2661,7 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
tools: [Tools.execute_code],
|
||||
stateful_code_sessions: true,
|
||||
stateful_code_environment: 'agent-user',
|
||||
git_identity: { name: 'LibreChat Agent', email: 'agent@example.com' },
|
||||
},
|
||||
toolNames: [AgentConstants.BASH_TOOL],
|
||||
toolRegistry,
|
||||
|
|
@ -2670,6 +2671,7 @@ describe('ToolService - Action Capability Gating', () => {
|
|||
expect(mockCreateAttachedWorkspaceBashTool).toHaveBeenCalledWith({
|
||||
authHeaders: expect.any(Function),
|
||||
baseUrl: 'http://attached-code.test/v1',
|
||||
gitIdentity: { name: 'LibreChat Agent', email: 'agent@example.com' },
|
||||
});
|
||||
expect(result.loadedTools).toContainEqual({ name: AgentConstants.BASH_TOOL });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ export type AgentForm = {
|
|||
stateful_code_environment?: StatefulCodeEnvironment;
|
||||
/** Operator-configured managed or attached execution environment. */
|
||||
code_environment_id?: string | null;
|
||||
/** Git authorship applied to sandboxed commands for this agent. */
|
||||
git_identity?: Agent['git_identity'];
|
||||
provider?: AgentProvider | OptionWithIcon;
|
||||
/** @deprecated Use edges instead */
|
||||
agent_ids?: string[];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
AgentCapabilities,
|
||||
agentGitIdentitySchema,
|
||||
STATEFUL_CODE_ENVIRONMENTS,
|
||||
resolveStatefulCodeEnvironment,
|
||||
resolveAllowedStatefulCodeEnvironments,
|
||||
|
|
@ -17,6 +19,7 @@ import {
|
|||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
CircleHelpIcon,
|
||||
Input,
|
||||
} from '@librechat/client';
|
||||
import type { StatefulCodeEnvironment } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
|
|
@ -36,7 +39,14 @@ export default function StatefulSessions() {
|
|||
const { user } = useAuthContext();
|
||||
const { agentsConfig } = useGetAgentsConfig();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { setValue, watch } = methods;
|
||||
const {
|
||||
register,
|
||||
unregister,
|
||||
getValues,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = methods;
|
||||
|
||||
const enabled = watch(AgentCapabilities.stateful_code_sessions) ?? false;
|
||||
const codeEnabled = watch(AgentCapabilities.execute_code);
|
||||
|
|
@ -45,6 +55,32 @@ export default function StatefulSessions() {
|
|||
const configuredEnvironments = agentsConfig?.statefulCodeSessions?.allowedEnvironments;
|
||||
const executionEnvironments = agentsConfig?.statefulCodeSessions?.environments ?? [];
|
||||
const allowedEnvironments = resolveAllowedStatefulCodeEnvironments(configuredEnvironments);
|
||||
const effectiveExecutionEnvironment = codeEnvironmentId
|
||||
? executionEnvironments.find((candidate) => candidate.id === codeEnvironmentId)
|
||||
: executionEnvironments.find((candidate) => candidate.default === true);
|
||||
const showGitIdentity =
|
||||
codeEnabled && enabled && effectiveExecutionEnvironment?.type === 'attached';
|
||||
const releaseIdentity = useCallback(() => {
|
||||
const identity = getValues('git_identity');
|
||||
const empty = !identity?.name?.trim() && !identity?.email?.trim();
|
||||
unregister('git_identity', {
|
||||
keepValue: empty || agentGitIdentitySchema.safeParse(identity).success,
|
||||
keepDefaultValue: true,
|
||||
});
|
||||
}, [getValues, unregister]);
|
||||
useEffect(() => {
|
||||
if (!showGitIdentity) releaseIdentity();
|
||||
}, [showGitIdentity, releaseIdentity]);
|
||||
useEffect(() => releaseIdentity, [releaseIdentity]);
|
||||
const validateGitIdentity = (_value: string | undefined, values: AgentForm) => {
|
||||
const identity = values.git_identity;
|
||||
const empty = !identity?.name?.trim() && !identity?.email?.trim();
|
||||
return (
|
||||
empty ||
|
||||
agentGitIdentitySchema.safeParse(identity).success ||
|
||||
localize('com_ui_agent_git_identity_both_required')
|
||||
);
|
||||
};
|
||||
|
||||
const handleChange = (value: boolean) => {
|
||||
setValue(AgentCapabilities.stateful_code_sessions, value, { shouldDirty: true });
|
||||
|
|
@ -170,6 +206,40 @@ export default function StatefulSessions() {
|
|||
<p className="text-xs text-text-tertiary">
|
||||
{localize('com_nav_info_stateful_code_environment')}
|
||||
</p>
|
||||
{showGitIdentity && (
|
||||
<div className="space-y-2 border-t border-border-light pt-3">
|
||||
<div className="text-xs font-medium text-text-secondary">
|
||||
{localize('com_ui_agent_git_identity')}
|
||||
</div>
|
||||
<Input
|
||||
{...register('git_identity.name', {
|
||||
validate: validateGitIdentity,
|
||||
deps: ['git_identity.email'],
|
||||
})}
|
||||
maxLength={128}
|
||||
placeholder={localize('com_ui_agent_git_name')}
|
||||
aria-label={localize('com_ui_agent_git_name')}
|
||||
/>
|
||||
<Input
|
||||
{...register('git_identity.email', {
|
||||
validate: validateGitIdentity,
|
||||
deps: ['git_identity.name'],
|
||||
})}
|
||||
type="email"
|
||||
maxLength={254}
|
||||
placeholder={localize('com_ui_agent_git_email')}
|
||||
aria-label={localize('com_ui_agent_git_email')}
|
||||
/>
|
||||
{(errors.git_identity?.name || errors.git_identity?.email) && (
|
||||
<p className="text-xs text-text-destructive" role="alert">
|
||||
{localize('com_ui_agent_git_identity_both_required')}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{localize('com_nav_info_agent_git_identity')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export function composeAgentUpdatePayload(
|
|||
stateful_code_sessions,
|
||||
stateful_code_environment,
|
||||
code_environment_id,
|
||||
git_identity,
|
||||
recursion_limit,
|
||||
category,
|
||||
support_contact,
|
||||
|
|
@ -122,6 +123,17 @@ export function composeAgentUpdatePayload(
|
|||
const model_parameters = modelParameterSettings
|
||||
? pruneAgentModelParameters(currentModelParameters, modelParameterSettings)
|
||||
: currentModelParameters;
|
||||
let normalizedGitIdentity: AgentUpdateParams['git_identity'];
|
||||
const gitIdentityName = git_identity?.name?.trim() ?? '';
|
||||
const gitIdentityEmail = git_identity?.email?.trim() ?? '';
|
||||
if (gitIdentityName || gitIdentityEmail) {
|
||||
normalizedGitIdentity = {
|
||||
name: gitIdentityName,
|
||||
email: gitIdentityEmail,
|
||||
};
|
||||
} else if (agent_id && git_identity != null) {
|
||||
normalizedGitIdentity = null;
|
||||
}
|
||||
|
||||
return {
|
||||
payload: {
|
||||
|
|
@ -140,6 +152,7 @@ export function composeAgentUpdatePayload(
|
|||
stateful_code_sessions: normalizedStatefulCodeSessions,
|
||||
stateful_code_environment: normalizedStatefulCodeEnvironment,
|
||||
code_environment_id: agent_id ? code_environment_id : (code_environment_id ?? undefined),
|
||||
git_identity: normalizedGitIdentity,
|
||||
recursion_limit,
|
||||
category,
|
||||
support_contact,
|
||||
|
|
@ -640,7 +653,13 @@ export default function AgentPanel() {
|
|||
});
|
||||
}
|
||||
|
||||
create.mutate({ ...basePayload, model, tools, provider });
|
||||
create.mutate({
|
||||
...basePayload,
|
||||
git_identity: basePayload.git_identity ?? undefined,
|
||||
model,
|
||||
tools,
|
||||
provider,
|
||||
});
|
||||
},
|
||||
[
|
||||
agent_id,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ function AgentSelect({
|
|||
avatar_action: null,
|
||||
stateful_code_environment: fullAgent.stateful_code_environment ?? 'user',
|
||||
code_environment_id: fullAgent.code_environment_id,
|
||||
git_identity: fullAgent.git_identity,
|
||||
};
|
||||
|
||||
Object.entries(fullAgent).forEach(([name, value]) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @jest-environment jsdom
|
||||
*/
|
||||
import { Constants, EModelEndpoint, type Agent } from 'librechat-data-provider';
|
||||
import type { AgentModelParameters } from 'librechat-data-provider';
|
||||
import type {
|
||||
AgentCreateParams,
|
||||
AgentUpdateParams,
|
||||
AgentModelParameters,
|
||||
} from 'librechat-data-provider';
|
||||
import type { FieldNamesMarkedBoolean } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import {
|
||||
|
|
@ -13,6 +17,13 @@ import {
|
|||
mayHavePersistedChange,
|
||||
} from '../AgentPanel';
|
||||
|
||||
test('the create identity contract excludes the update-only clear sentinel', () => {
|
||||
const createAcceptsNull: null extends AgentCreateParams['git_identity'] ? true : false = false;
|
||||
const updateAcceptsNull: null extends AgentUpdateParams['git_identity'] ? true : false = true;
|
||||
expect(createAcceptsNull).toBe(false);
|
||||
expect(updateAcceptsNull).toBe(true);
|
||||
});
|
||||
|
||||
const createForm = (): AgentForm => ({
|
||||
agent: undefined,
|
||||
id: 'agent_123',
|
||||
|
|
@ -160,6 +171,45 @@ describe('composeAgentUpdatePayload', () => {
|
|||
expect(payload.code_environment_id).toBeUndefined();
|
||||
});
|
||||
|
||||
it('normalizes a configured Git identity', () => {
|
||||
const form = createForm();
|
||||
form.git_identity = { name: ' Coding Agent ', email: ' agent@example.com ' };
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
|
||||
|
||||
expect(payload.git_identity).toEqual({
|
||||
name: 'Coding Agent',
|
||||
email: 'agent@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('clears an empty Git identity when updating an agent', () => {
|
||||
const form = createForm();
|
||||
form.git_identity = { name: '', email: '' };
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
|
||||
|
||||
expect(payload.git_identity).toBeNull();
|
||||
});
|
||||
|
||||
it('does not turn a partially filled Git identity into a clear operation', () => {
|
||||
const form = createForm();
|
||||
form.git_identity = { name: 'Coding Agent', email: '' };
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
|
||||
|
||||
expect(payload.git_identity).toEqual({ name: 'Coding Agent', email: '' });
|
||||
});
|
||||
|
||||
it('omits an empty Git identity when creating an agent', () => {
|
||||
const form = createForm();
|
||||
form.git_identity = { name: '', email: '' };
|
||||
|
||||
const { payload } = composeAgentUpdatePayload(form);
|
||||
|
||||
expect(payload.git_identity).toBeUndefined();
|
||||
});
|
||||
|
||||
it('persists standalone skill authoring separately from catalog access', () => {
|
||||
const form = createForm();
|
||||
form.skills = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
/** @jest-environment jsdom */
|
||||
/* eslint-disable i18next/no-literal-string -- Test harness controls are not product UI. */
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { AgentCapabilities } from 'librechat-data-provider';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import type { AgentForm } from '~/common';
|
||||
import StatefulSessions from '../Advanced/StatefulSessions';
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: () => (key: string) => key,
|
||||
useAuthContext: () => ({ user: {} }),
|
||||
useGetAgentsConfig: () => ({
|
||||
agentsConfig: {
|
||||
statefulCodeSessions: {
|
||||
environments: [{ id: 'byom', name: 'My machine', type: 'attached', default: true }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
function IdentityForm({ savedIdentity }: { savedIdentity?: AgentForm['git_identity'] }) {
|
||||
const [advanced, setAdvanced] = useState(true);
|
||||
const methods = useForm<AgentForm>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
execute_code: true,
|
||||
stateful_code_sessions: true,
|
||||
git_identity: savedIdentity,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
{advanced && <StatefulSessions />}
|
||||
<button onClick={() => setAdvanced(!advanced)}>Toggle Advanced</button>
|
||||
<button onClick={() => methods.setValue(AgentCapabilities.execute_code, false)}>
|
||||
Disable code
|
||||
</button>
|
||||
<button onClick={() => methods.setValue('code_environment_id', 'managed')}>
|
||||
Use managed
|
||||
</button>
|
||||
<button onClick={() => methods.setValue(AgentCapabilities.stateful_code_sessions, false)}>
|
||||
Disable sessions
|
||||
</button>
|
||||
<output data-testid="identity">{JSON.stringify(methods.watch('git_identity'))}</output>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
test.each(['', 'not-an-email'])(
|
||||
'restores the saved identity when reopening an invalid draft: %s',
|
||||
async (email) => {
|
||||
render(<IdentityForm savedIdentity={{ name: 'Saved Agent', email: 'saved@example.com' }} />);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_email'), {
|
||||
target: { value: email },
|
||||
});
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Toggle Advanced'));
|
||||
fireEvent.click(screen.getByText('Toggle Advanced'));
|
||||
expect(screen.getByLabelText('com_ui_agent_git_name')).toHaveValue('Saved Agent');
|
||||
expect(screen.getByLabelText('com_ui_agent_git_email')).toHaveValue('saved@example.com');
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
test('clears cross-field errors when completing or clearing a Git identity', async () => {
|
||||
render(<IdentityForm />);
|
||||
const name = screen.getByLabelText('com_ui_agent_git_name');
|
||||
const email = screen.getByLabelText('com_ui_agent_git_email');
|
||||
fireEvent.change(name, { target: { value: 'Coding Agent' } });
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.change(email, { target: { value: 'agent@example.com' } });
|
||||
await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument());
|
||||
fireEvent.change(name, { target: { value: '' } });
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.change(email, { target: { value: '' } });
|
||||
await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test.each(['Disable code', 'Use managed', 'Disable sessions', 'Toggle Advanced'])(
|
||||
'discards a partial identity when its controls disappear: %s',
|
||||
async (action) => {
|
||||
render(<IdentityForm />);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_name'), {
|
||||
target: { value: 'Partial Agent' },
|
||||
});
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(action));
|
||||
await waitFor(() => expect(screen.getByTestId('identity')).toBeEmptyDOMElement());
|
||||
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
test.each(['Disable code', 'Use managed', 'Disable sessions', 'Toggle Advanced'])(
|
||||
'discards an invalid email when its controls disappear: %s',
|
||||
async (action) => {
|
||||
render(<IdentityForm />);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_name'), {
|
||||
target: { value: 'Coding Agent' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_email'), {
|
||||
target: { value: 'not-an-email' },
|
||||
});
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(action));
|
||||
await waitFor(() => expect(screen.getByTestId('identity')).toBeEmptyDOMElement());
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
{ name: 'Coding Agent', email: 'agent@example.com' },
|
||||
{ name: '', email: '' },
|
||||
])('retains a valid identity or explicit clear through panel navigation: %j', async (identity) => {
|
||||
render(<IdentityForm />);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_name'), {
|
||||
target: { value: identity.name },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_email'), {
|
||||
target: { value: identity.email },
|
||||
});
|
||||
fireEvent.click(screen.getByText('Toggle Advanced'));
|
||||
expect(screen.getByTestId('identity')).toHaveTextContent(JSON.stringify(identity));
|
||||
fireEvent.click(screen.getByText('Toggle Advanced'));
|
||||
expect(screen.getByLabelText('com_ui_agent_git_name')).toHaveValue(identity.name);
|
||||
expect(screen.getByLabelText('com_ui_agent_git_email')).toHaveValue(identity.email);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_email'), {
|
||||
target: { value: 'invalid' },
|
||||
});
|
||||
expect(await screen.findByRole('alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('preserves a complete identity when switching away from the attached environment', async () => {
|
||||
render(<IdentityForm />);
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_name'), {
|
||||
target: { value: 'Coding Agent' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('com_ui_agent_git_email'), {
|
||||
target: { value: 'agent@example.com' },
|
||||
});
|
||||
await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument());
|
||||
fireEvent.click(screen.getByText('Use managed'));
|
||||
expect(screen.getByTestId('identity')).toHaveTextContent('Coding Agent');
|
||||
expect(screen.getByTestId('identity')).toHaveTextContent('agent@example.com');
|
||||
});
|
||||
|
|
@ -2106,6 +2106,11 @@
|
|||
"com_ui_skill_instructions": "Instructions",
|
||||
"com_ui_skill_instructions_placeholder": "Enter your skill instructions in markdown...",
|
||||
"com_ui_skill_name_invalid": "Use lowercase letters, digits, and dashes only (kebab-case)",
|
||||
"com_ui_agent_git_identity": "Git identity",
|
||||
"com_ui_agent_git_identity_both_required": "Enter a valid Git name and email, or leave both blank.",
|
||||
"com_ui_agent_git_name": "Commit name",
|
||||
"com_ui_agent_git_email": "Commit email",
|
||||
"com_nav_info_agent_git_identity": "Applied as the author and committer for Git commands run by this agent. Credentials are configured separately.",
|
||||
"com_ui_skill_name_placeholder": "brand-guidelines",
|
||||
"com_ui_skill_name_required": "Name is required",
|
||||
"com_ui_skill_name_too_long": "Name must be {{0}} characters or fewer",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,20 @@ describe('getLazySubagentConfigId', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('changes when the Git author identity changes', () => {
|
||||
expect(
|
||||
getLazySubagentConfigId({
|
||||
...agent,
|
||||
git_identity: { name: 'First Agent', email: 'first@example.com' },
|
||||
}),
|
||||
).not.toBe(
|
||||
getLazySubagentConfigId({
|
||||
...agent,
|
||||
git_identity: { name: 'Second Agent', email: 'second@example.com' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('is stable across key order and excludes secret values', () => {
|
||||
const first = getLazySubagentConfigId({
|
||||
...agent,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type VersionedAgent = Pick<
|
|||
| 'stateful_code_sessions'
|
||||
| 'stateful_code_environment'
|
||||
| 'code_environment_id'
|
||||
| 'git_identity'
|
||||
| 'artifacts'
|
||||
| 'recursion_limit'
|
||||
| 'agent_ids'
|
||||
|
|
@ -95,6 +96,7 @@ export function selectLazySubagentConfig(agent: VersionedAgent): Omit<VersionedA
|
|||
stateful_code_sessions,
|
||||
stateful_code_environment,
|
||||
code_environment_id,
|
||||
git_identity,
|
||||
artifacts,
|
||||
recursion_limit,
|
||||
agent_ids,
|
||||
|
|
@ -127,6 +129,7 @@ export function selectLazySubagentConfig(agent: VersionedAgent): Omit<VersionedA
|
|||
stateful_code_sessions,
|
||||
stateful_code_environment,
|
||||
code_environment_id,
|
||||
git_identity,
|
||||
artifacts,
|
||||
recursion_limit,
|
||||
agent_ids,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe('Agent Management contract', () => {
|
|||
model: 'gpt-5',
|
||||
name: 'Researcher',
|
||||
stateful_code_environment: 'agent-user',
|
||||
git_identity: { name: 'LibreChat Agent', email: 'agent@example.com' },
|
||||
subagents: { enabled: true, allowSelf: true, agent_ids: [] },
|
||||
}),
|
||||
).toMatchObject({ provider: 'openAI', model: 'gpt-5', tools: [] });
|
||||
|
|
@ -56,11 +57,13 @@ describe('Agent Management contract', () => {
|
|||
instructions: 'Updated',
|
||||
model_parameters: { temperature: 0.1 },
|
||||
code_environment_id: null,
|
||||
git_identity: { name: 'LibreChat Agent', email: 'agent@example.com' },
|
||||
}),
|
||||
).toEqual({
|
||||
instructions: 'Updated',
|
||||
model_parameters: { temperature: 0.1 },
|
||||
code_environment_id: null,
|
||||
git_identity: { name: 'LibreChat Agent', email: 'agent@example.com' },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ export function projectAgentManagementResponse(
|
|||
stateful_code_sessions: source.stateful_code_sessions,
|
||||
stateful_code_environment: source.stateful_code_environment,
|
||||
code_environment_id: source.code_environment_id,
|
||||
git_identity: source.git_identity,
|
||||
artifacts: source.artifacts,
|
||||
recursion_limit: source.recursion_limit,
|
||||
conversation_starters: source.conversation_starters,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,32 @@ import {
|
|||
validateAgentModel,
|
||||
} from './validation';
|
||||
|
||||
describe('agent Git identity validation', () => {
|
||||
const base = { provider: 'openAI', model: 'gpt-4o-mini', tools: [] };
|
||||
|
||||
it('accepts and trims a valid commit identity', () => {
|
||||
expect(
|
||||
agentCreateSchema.parse({
|
||||
...base,
|
||||
git_identity: { name: ' Coding Agent ', email: ' agent@example.com ' },
|
||||
}).git_identity,
|
||||
).toEqual({ name: 'Coding Agent', email: 'agent@example.com' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: '', email: 'agent@example.com' },
|
||||
{ name: 'Coding Agent\nInjected', email: 'agent@example.com' },
|
||||
{ name: 'Coding Agent', email: 'not-an-email' },
|
||||
])('rejects an unsafe or incomplete identity: %j', (git_identity) => {
|
||||
expect(agentCreateSchema.safeParse({ ...base, git_identity }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts null only when updating to clear a configured identity', () => {
|
||||
expect(agentCreateSchema.safeParse({ ...base, git_identity: null }).success).toBe(false);
|
||||
expect(agentUpdateSchema.parse({ git_identity: null })).toEqual({ git_identity: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('agentSubagentsSchema', () => {
|
||||
const graph = {
|
||||
type: 'research_team',
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@ import {
|
|||
MemoryScope,
|
||||
SkillsScope,
|
||||
getMaxSubagents,
|
||||
agentGitIdentitySchema,
|
||||
resolveModelCatalogKey,
|
||||
ViolationTypes,
|
||||
ErrorTypes,
|
||||
MAX_SUBAGENT_GRAPH_NODES,
|
||||
MAX_GRAPH_SUBAGENT_MEMBERS,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Agent, TModelsConfig, AgentSubagentsConfig } from 'librechat-data-provider';
|
||||
import type {
|
||||
Agent,
|
||||
AgentGitIdentity,
|
||||
TModelsConfig,
|
||||
AgentSubagentsConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
/**
|
||||
|
|
@ -402,6 +408,8 @@ export const agentSubagentsSchema: z.ZodOptional<z.ZodType<AgentSubagentsConfig>
|
|||
|
||||
/** Base agent schema with all common fields */
|
||||
const agentCodeEnvironmentIdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
|
||||
const agentGitIdentityUpdateSchema: z.ZodType<AgentGitIdentity | null | undefined> =
|
||||
agentGitIdentitySchema.nullable();
|
||||
|
||||
export const agentBaseSchema: z.ZodObject<
|
||||
{
|
||||
|
|
@ -492,6 +500,7 @@ export const agentBaseSchema: z.ZodObject<
|
|||
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
|
||||
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
|
||||
code_environment_id: z.ZodOptional<z.ZodString>;
|
||||
git_identity: typeof agentGitIdentitySchema;
|
||||
artifacts: z.ZodOptional<z.ZodString>;
|
||||
recursion_limit: z.ZodOptional<z.ZodNumber>;
|
||||
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
|
||||
|
|
@ -567,6 +576,7 @@ export const agentBaseSchema: z.ZodObject<
|
|||
stateful_code_sessions: z.boolean().optional(),
|
||||
stateful_code_environment: z.enum(['user', 'agent-user', 'conversation']).optional(),
|
||||
code_environment_id: agentCodeEnvironmentIdSchema.optional(),
|
||||
git_identity: agentGitIdentitySchema,
|
||||
artifacts: z.string().optional(),
|
||||
recursion_limit: z.number().optional(),
|
||||
conversation_starters: z.array(z.string()).optional(),
|
||||
|
|
@ -665,6 +675,7 @@ export const agentCreateSchema: z.ZodObject<
|
|||
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
|
||||
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
|
||||
code_environment_id: z.ZodOptional<z.ZodString>;
|
||||
git_identity: typeof agentGitIdentitySchema;
|
||||
artifacts: z.ZodOptional<z.ZodString>;
|
||||
recursion_limit: z.ZodOptional<z.ZodNumber>;
|
||||
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
|
||||
|
|
@ -799,6 +810,7 @@ export const agentUpdateSchema: z.ZodObject<
|
|||
stateful_code_sessions: z.ZodOptional<z.ZodBoolean>;
|
||||
stateful_code_environment: z.ZodOptional<z.ZodEnum<['user', 'agent-user', 'conversation']>>;
|
||||
code_environment_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
git_identity: typeof agentGitIdentityUpdateSchema;
|
||||
artifacts: z.ZodOptional<z.ZodString>;
|
||||
recursion_limit: z.ZodOptional<z.ZodNumber>;
|
||||
conversation_starters: z.ZodOptional<z.ZodArray<z.ZodString, 'many'>>;
|
||||
|
|
@ -883,6 +895,7 @@ export const agentUpdateSchema: z.ZodObject<
|
|||
> = agentBaseSchema.extend({
|
||||
avatar: z.union([agentAvatarSchema, z.null()]).optional(),
|
||||
code_environment_id: agentCodeEnvironmentIdSchema.nullable().optional(),
|
||||
git_identity: agentGitIdentityUpdateSchema,
|
||||
provider: z.string().optional(),
|
||||
model: z.string().nullable().optional(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,50 @@
|
|||
import { once } from 'node:events';
|
||||
import { createServer } from 'node:http';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
import type { CodeBridgeFetch } from './bridge';
|
||||
import { ATTACHED_WORKSPACE_BASH_SCHEMA, createAttachedWorkspaceBashTool } from './command';
|
||||
import {
|
||||
ATTACHED_WORKSPACE_BASH_SCHEMA,
|
||||
createAttachedWorkspaceBashTool,
|
||||
createGitIdentityProgrammaticBashTool,
|
||||
} from './command';
|
||||
|
||||
describe('programmatic Bash Git identity', () => {
|
||||
test('applies authorship before the SDK sends a programmatic script', async () => {
|
||||
let receivedCode = '';
|
||||
const server = createServer(async (req, res) => {
|
||||
let body = '';
|
||||
for await (const chunk of req) body += chunk;
|
||||
receivedCode = JSON.parse(body).code;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({ status: 'completed', stdout: 'done', stderr: '', files: [] }));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
const { port } = server.address() as AddressInfo;
|
||||
const bashTool = createGitIdentityProgrammaticBashTool(
|
||||
{ baseUrl: `http://127.0.0.1:${port}/v1`, authHeaders: () => ({}) },
|
||||
{ name: "Agent O'Brien", email: 'agent@example.com' },
|
||||
);
|
||||
try {
|
||||
const invocationConfig = {
|
||||
tags: [],
|
||||
toolCall: { toolDefs: [] },
|
||||
};
|
||||
await bashTool.func(
|
||||
{ code: 'git commit -m feature', tool_manifest: [] },
|
||||
undefined,
|
||||
invocationConfig,
|
||||
);
|
||||
expect(receivedCode).toContain(`GIT_AUTHOR_NAME='Agent O'"'"'Brien'`);
|
||||
expect(receivedCode).toContain("GIT_COMMITTER_EMAIL='agent@example.com'");
|
||||
expect(receivedCode).toContain('git commit -m feature');
|
||||
expect(receivedCode).not.toContain('git config');
|
||||
} finally {
|
||||
server.close();
|
||||
await once(server, 'close');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function commandResponse(overrides: Record<string, unknown> = {}): Response {
|
||||
return new Response(
|
||||
|
|
@ -86,6 +131,24 @@ describe('createAttachedWorkspaceBashTool', () => {
|
|||
expect(request.command).toBe(`bash -c 'printf "%s" "$1"' -- 'a'"'"'b; echo unsafe'`);
|
||||
});
|
||||
|
||||
test('injects the configured agent Git identity without writing machine Git configuration', async () => {
|
||||
const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse());
|
||||
const bashTool = createAttachedWorkspaceBashTool({
|
||||
baseUrl: 'https://code.example.com/v1',
|
||||
authHeaders: () => ({}),
|
||||
gitIdentity: { name: "Agent O'Brien", email: 'agent@example.com' },
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
await bashTool.func({ command: 'git commit -m "Implement feature"' }, undefined, {});
|
||||
|
||||
const request = JSON.parse(String((fetchImpl as jest.Mock).mock.calls[0][1]?.body));
|
||||
expect(request.command).toBe(
|
||||
`export GIT_AUTHOR_NAME='Agent O'"'"'Brien' GIT_AUTHOR_EMAIL='agent@example.com' GIT_COMMITTER_NAME='Agent O'"'"'Brien' GIT_COMMITTER_EMAIL='agent@example.com'; git commit -m "Implement feature"`,
|
||||
);
|
||||
expect(request.command).not.toContain('git config');
|
||||
});
|
||||
|
||||
test('reports termination, timeouts, and truncation without hiding stderr', async () => {
|
||||
const fetchImpl: CodeBridgeFetch = jest.fn(async () =>
|
||||
commandResponse({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { tool } from '@librechat/agents/langchain/tools';
|
||||
import { BashExecutionToolDefinition, BashToolOutputReferencesGuide } from '@librechat/agents';
|
||||
import {
|
||||
BashExecutionToolDefinition,
|
||||
BashToolOutputReferencesGuide,
|
||||
createBashProgrammaticToolCallingTool,
|
||||
} from '@librechat/agents';
|
||||
import type { DynamicStructuredTool } from '@librechat/agents/langchain/tools';
|
||||
import type { AgentGitIdentity } from 'librechat-data-provider';
|
||||
import type { LCTool } from '@librechat/agents';
|
||||
import type { WorkspaceExecuteCommandResult } from './workspace';
|
||||
import type { CodeBridgeFetch } from './bridge';
|
||||
|
|
@ -59,6 +64,41 @@ function commandWithArguments(command: string, args: string[] | undefined): stri
|
|||
return `bash -c ${quoteShellArgument(command)} -- ${args.map(quoteShellArgument).join(' ')}`;
|
||||
}
|
||||
|
||||
function commandWithGitIdentity(
|
||||
command: string,
|
||||
identity: AgentGitIdentity | null | undefined,
|
||||
): string {
|
||||
if (identity == null) return command;
|
||||
const name = identity.name.trim();
|
||||
const email = identity.email.trim();
|
||||
if (
|
||||
name.length === 0 ||
|
||||
name.length > 128 ||
|
||||
email.length === 0 ||
|
||||
email.length > 254 ||
|
||||
/[\0\r\n]/.test(name) ||
|
||||
/[\0\r\n]/.test(email)
|
||||
) {
|
||||
throw new Error('Invalid agent Git identity');
|
||||
}
|
||||
return `export GIT_AUTHOR_NAME=${quoteShellArgument(name)} GIT_AUTHOR_EMAIL=${quoteShellArgument(email)} GIT_COMMITTER_NAME=${quoteShellArgument(name)} GIT_COMMITTER_EMAIL=${quoteShellArgument(email)}; ${command}`;
|
||||
}
|
||||
|
||||
/** Apply authorship before the SDK prepares the script and its replay requests. */
|
||||
export function createGitIdentityProgrammaticBashTool(
|
||||
options: Parameters<typeof createBashProgrammaticToolCallingTool>[0],
|
||||
identity?: AgentGitIdentity | null,
|
||||
): DynamicStructuredTool {
|
||||
const bashTool = createBashProgrammaticToolCallingTool(options);
|
||||
if (identity == null) return bashTool;
|
||||
const execute = bashTool.func.bind(bashTool);
|
||||
bashTool.func = (input, ...args) => {
|
||||
const params = input as { code: string };
|
||||
return execute({ ...params, code: commandWithGitIdentity(params.code, identity) }, ...args);
|
||||
};
|
||||
return bashTool;
|
||||
}
|
||||
|
||||
function formatCommandResult(result: WorkspaceExecuteCommandResult): string {
|
||||
let output = '';
|
||||
if (result.stdout.length > 0) output += `stdout:\n${result.stdout}\n`;
|
||||
|
|
@ -75,11 +115,13 @@ export function createAttachedWorkspaceBashTool({
|
|||
baseUrl,
|
||||
authHeaders,
|
||||
workspaceId = DEFAULT_WORKSPACE_ID,
|
||||
gitIdentity,
|
||||
fetchImpl,
|
||||
}: {
|
||||
baseUrl: string;
|
||||
authHeaders: () => Promise<Record<string, string>> | Record<string, string>;
|
||||
workspaceId?: string;
|
||||
gitIdentity?: AgentGitIdentity | null;
|
||||
fetchImpl?: CodeBridgeFetch;
|
||||
}): DynamicStructuredTool {
|
||||
return tool(
|
||||
|
|
@ -87,7 +129,10 @@ export function createAttachedWorkspaceBashTool({
|
|||
rawInput: { command: string; args?: string[]; intent?: string },
|
||||
config,
|
||||
): Promise<[string, Record<string, never>]> => {
|
||||
const command = commandWithArguments(rawInput.command, rawInput.args);
|
||||
const command = commandWithGitIdentity(
|
||||
commandWithArguments(rawInput.command, rawInput.args),
|
||||
gitIdentity,
|
||||
);
|
||||
const result = await executeWorkspaceTool({
|
||||
baseURL: baseUrl,
|
||||
authHeaders: await authHeaders(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import type { OpenAPIV3 } from 'openapi-types';
|
||||
import type { AssistantsEndpoint, AgentProvider, MemoryScope, SkillsScope } from 'src/schemas';
|
||||
import type { StatefulCodeEnvironment } from '../stateful-code';
|
||||
|
|
@ -307,6 +308,30 @@ export type AgentSubagentsConfig = {
|
|||
graphs?: AgentSubagentGraph[];
|
||||
};
|
||||
|
||||
export type AgentGitIdentity = {
|
||||
/** Commit author and committer display name. */
|
||||
name: string;
|
||||
/** Commit author and committer email address. */
|
||||
email: string;
|
||||
};
|
||||
|
||||
export const agentGitIdentitySchema: z.ZodType<AgentGitIdentity | undefined> = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.refine((value) => !/[\0\r\n]/.test(value)),
|
||||
email: z
|
||||
.string()
|
||||
.trim()
|
||||
.email()
|
||||
.max(254)
|
||||
.refine((value) => !/[\0\r\n]/.test(value)),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type Agent = {
|
||||
_id?: string;
|
||||
id: string;
|
||||
|
|
@ -339,6 +364,8 @@ export type Agent = {
|
|||
stateful_code_environment?: StatefulCodeEnvironment;
|
||||
/** Operator-configured managed or attached stateful execution environment. */
|
||||
code_environment_id?: string | null;
|
||||
/** Non-secret Git authorship injected into this agent's sandboxed commands. */
|
||||
git_identity?: AgentGitIdentity | null;
|
||||
artifacts?: ArtifactModes;
|
||||
recursion_limit?: number;
|
||||
isPublic?: boolean;
|
||||
|
|
@ -381,6 +408,7 @@ export type Agent = {
|
|||
export type TAgentsMap = Record<string, Agent | undefined>;
|
||||
|
||||
export type AgentCreateParams = {
|
||||
git_identity?: AgentGitIdentity;
|
||||
name?: string | null;
|
||||
description?: string | null;
|
||||
avatar?: AgentAvatar | null;
|
||||
|
|
@ -432,6 +460,7 @@ export type AgentUpdateParams = {
|
|||
| 'stateful_code_sessions'
|
||||
| 'stateful_code_environment'
|
||||
| 'code_environment_id'
|
||||
| 'git_identity'
|
||||
| 'artifacts'
|
||||
| 'recursion_limit'
|
||||
| 'category'
|
||||
|
|
|
|||
|
|
@ -2705,6 +2705,27 @@ describe('Agent Methods', () => {
|
|||
expect(revertedAgent.code_environment_id).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should clear a Git identity absent from the restored version', async () => {
|
||||
const agentId = `agent_${uuidv4()}`;
|
||||
const authorId = new mongoose.Types.ObjectId();
|
||||
|
||||
await createAgent({
|
||||
id: agentId,
|
||||
name: 'Legacy Git Identity Agent',
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
author: authorId,
|
||||
});
|
||||
await updateAgent(
|
||||
{ id: agentId },
|
||||
{ git_identity: { name: 'Coding Agent', email: 'agent@example.com' } },
|
||||
);
|
||||
|
||||
const revertedAgent = await revertAgentVersion({ id: agentId }, 0);
|
||||
|
||||
expect(revertedAgent.git_identity).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should clear skill fields absent from a restored legacy version', async () => {
|
||||
const agentId = `agent_${uuidv4()}`;
|
||||
const authorId = new mongoose.Types.ObjectId();
|
||||
|
|
|
|||
|
|
@ -1435,7 +1435,12 @@ export function createAgentMethods(
|
|||
}
|
||||
|
||||
const unsetOnRestore: Record<string, 1> = {};
|
||||
for (const field of ['code_environment_id', 'skills_scope', 'skill_authoring_enabled']) {
|
||||
for (const field of [
|
||||
'code_environment_id',
|
||||
'git_identity',
|
||||
'skills_scope',
|
||||
'skill_authoring_enabled',
|
||||
]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(revertToVersion, field)) {
|
||||
unsetOnRestore[field] = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ const agentSchema: Schema<IAgent> = new Schema<IAgent>(
|
|||
code_environment_id: {
|
||||
type: String,
|
||||
},
|
||||
git_identity: {
|
||||
type: new Schema(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true },
|
||||
},
|
||||
{ _id: false },
|
||||
),
|
||||
default: undefined,
|
||||
},
|
||||
/** @deprecated Use edges instead */
|
||||
agent_ids: {
|
||||
type: [String],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type {
|
|||
AgentToolOptions,
|
||||
AgentToolResources,
|
||||
AgentSubagentsConfig,
|
||||
AgentGitIdentity,
|
||||
} from 'librechat-data-provider';
|
||||
|
||||
export interface ISupportContact {
|
||||
|
|
@ -42,6 +43,7 @@ export interface IAgent extends Omit<Document, 'model'> {
|
|||
stateful_code_sessions?: boolean;
|
||||
stateful_code_environment?: 'user' | 'agent-user' | 'conversation';
|
||||
code_environment_id?: string;
|
||||
git_identity?: AgentGitIdentity | null;
|
||||
/** @deprecated Use edges instead */
|
||||
agent_ids?: string[];
|
||||
edges?: GraphEdge[];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue