🪶 feat: Enable Standalone Skill Authoring (#15480)

* feat: enable standalone skill authoring

* test: align skill e2e flow with enable toggle

* fix: preserve legacy skill state on version restore
This commit is contained in:
Danny Avila 2026-09-01 18:39:05 -04:00 • committed by GitHub
parent dfb264811c
commit aa7539909b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 683 additions and 123 deletions

View file

@ -429,6 +429,8 @@ class AgentClient extends BaseClient {
subagents: agent.subagents,
memory_scope: agent.memory_scope,
skills_enabled: agent.skills_enabled,
skill_authoring_enabled: agent.skill_authoring_enabled,
skills_scope: agent.skills_scope,
skills: agent.skills,
backgroundToolNames: agent.backgroundToolNames,
intentToolNames: agent.intentToolNames,

View file

@ -41,6 +41,7 @@ const {
const {
Time,
Tools,
SkillsScope,
CacheKeys,
Constants,
FileSources,
@ -241,18 +242,38 @@ const blockFilteredAgentContent = async (req, res, agentData) => {
const sanitizeViewerSkillScope = (agent, accessibleSkillSet) => {
const skillScopeEnabled = agent.skills_enabled === true;
const configuredScope = agent.skills_scope;
delete agent.skills_enabled;
delete agent.skills_scope;
if (!skillScopeEnabled) {
delete agent.skills;
return agent;
}
const configuredSkills = Array.isArray(agent.skills) ? agent.skills : [];
if (configuredSkills.length === 0) {
// Empty allowlist means the viewer's full accessible catalog.
if (configuredScope === SkillsScope.none) {
delete agent.skills;
agent.skills_enabled = true;
agent.skills_scope = SkillsScope.none;
return agent;
}
if (configuredScope === SkillsScope.all) {
delete agent.skills;
agent.skills_enabled = true;
agent.skills_scope = SkillsScope.all;
return agent;
}
const configuredSkills = Array.isArray(agent.skills) ? agent.skills : [];
if (configuredSkills.length === 0) {
// Legacy empty allowlists mean the viewer's full accessible catalog;
// explicit selected scope remains an intentionally empty catalog.
delete agent.skills;
agent.skills_enabled = true;
if (configuredScope === SkillsScope.selected) {
agent.skills_scope = SkillsScope.selected;
}
return agent;
}
@ -262,11 +283,18 @@ const sanitizeViewerSkillScope = (agent, accessibleSkillSet) => {
if (visibleSkills.length === 0) {
delete agent.skills;
if (configuredScope === SkillsScope.selected) {
agent.skills_enabled = true;
agent.skills_scope = SkillsScope.selected;
}
return agent;
}
agent.skills = visibleSkills;
agent.skills_enabled = true;
if (configuredScope === SkillsScope.selected) {
agent.skills_scope = SkillsScope.selected;
}
return agent;
};

View file

@ -4,6 +4,7 @@ const { v4: uuidv4 } = require('uuid');
const { agentSchema, aclEntrySchema, fileSchema, userSchema } = require('@librechat/data-schemas');
const {
Tools,
SkillsScope,
FileSources,
PermissionBits,
PrincipalModel,
@ -3210,6 +3211,34 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
expect(response.data[0].skills_enabled).toBe(true);
});
test('should preserve an enabled empty catalog for VIEW list callers', async () => {
await Agent.findByIdAndUpdate(agentA1._id, {
skills_enabled: false,
skill_authoring_enabled: true,
skills_scope: SkillsScope.none,
skills: [],
});
mockReq.user.id = userB.toString();
mockReq.query.requiredPermission = String(PermissionBits.VIEW);
findAccessibleResources.mockImplementation(({ resourceType }) => {
if (resourceType === ResourceType.AGENT) {
return Promise.resolve([agentA1._id]);
}
return Promise.resolve([]);
});
findPubliclyAccessibleResources.mockResolvedValue([]);
await getListAgentsHandler(mockReq, mockRes);
const response = mockRes.json.mock.calls[0][0];
expect(response.data).toHaveLength(1);
expect(response.data[0].skills).toBeUndefined();
expect(response.data[0].skills_enabled).toBeUndefined();
expect(response.data[0].skill_authoring_enabled).toBe(true);
expect(response.data[0].skills_scope).toBeUndefined();
});
test('should return raw skill configuration for EDIT list callers', async () => {
const visibleSkillId = new mongoose.Types.ObjectId();
const hiddenSkillId = new mongoose.Types.ObjectId();