fix(security): scope skill assignments by company

This commit is contained in:
ZacharyZhang-NY committed 2026-05-14 23:37:58 -04:00
1 parent 41e1dec647
commit d1a3d23c70
8 files changed
+85 -22

No files matched your search

+14 -3
View File
@@ -95,7 +95,7 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
server.json(res, 404, { error: "Agent not found in your company" });
return;
}
const skillIds = db.core.skills.listAgentSkillAssignments(agentId);
const skillIds = db.core.skills.listAgentSkillAssignments(token.companyId, agentId);
const skills = skillIds
.map((sid) => db.core.skills.getCompanySkill(token.companyId, sid))
.filter((s): s is NonNullable<typeof s> => s != null);
@@ -132,7 +132,10 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
server.json(res, 404, { error: "Skill not found in your company" });
return;
}
db.core.skills.assignSkillToAgent(agentId, parsed.data.skillId);
if (!db.core.skills.assignSkillToAgent(token.companyId, agentId, parsed.data.skillId)) {
server.json(res, 404, { error: "Skill assignment targets not found in your company" });
return;
}
onDomainChanged();
server.json(res, 200, { ok: true, message: `Skill "${skill.name}" assigned to agent ${agent.name}` });
});
@@ -151,7 +154,15 @@ export function registerAgentControlRoutes(server: AgentApiServer) {
server.json(res, 404, { error: "Agent not found in your company" });
return;
}
db.core.skills.removeSkillFromAgent(agentId, skillId);
const skill = db.core.skills.getCompanySkill(token.companyId, skillId);
if (!skill) {
server.json(res, 404, { error: "Skill not found in your company" });
return;
}
if (!db.core.skills.removeSkillFromAgent(token.companyId, agentId, skillId)) {
server.json(res, 404, { error: "Skill assignment not found in your company" });
return;
}
onDomainChanged();
server.json(res, 200, { ok: true, message: `Skill removed from agent ${agent.name}` });
});
+48 -7
View File
@@ -7,6 +7,21 @@ export function buildSkillMethods(
getDb: () => DatabaseSync,
readAll: ReadAll,
) {
function assignmentTargetsBelongToCompany(companyId: string, agentId: string, skillId: string): boolean {
const row = getDb()
.prepare(`
SELECT 1 AS present
FROM agents a
JOIN company_skills cs ON cs.id = ?
WHERE a.id = ?
AND a.company_id = ?
AND cs.company_id = ?
LIMIT 1
`)
.get(skillId, agentId, companyId, companyId) as { present: number } | undefined;
return Boolean(row?.present);
}
function mapSkillRow(row: SqliteRow): SkillRecord {
return {
id: row.id,
@@ -88,29 +103,55 @@ export function buildSkillMethods(
return result.changes > 0;
},
assignSkillToAgent(agentId: string, skillId: string): void {
assignSkillToAgent(companyId: string, agentId: string, skillId: string): boolean {
if (!assignmentTargetsBelongToCompany(companyId, agentId, skillId)) {
return false;
}
getDb()
.prepare("INSERT OR IGNORE INTO agent_skill_assignments (agent_id, skill_id, created_at) VALUES (?, ?, ?)")
.run(agentId, skillId, nowIso());
return true;
},
removeSkillFromAgent(agentId: string, skillId: string): void {
getDb()
removeSkillFromAgent(companyId: string, agentId: string, skillId: string): boolean {
if (!assignmentTargetsBelongToCompany(companyId, agentId, skillId)) {
return false;
}
const result = getDb()
.prepare("DELETE FROM agent_skill_assignments WHERE agent_id = ? AND skill_id = ?")
.run(agentId, skillId);
return result.changes > 0;
},
listAgentSkillAssignments(agentId: string): string[] {
listAgentSkillAssignments(companyId: string, agentId: string): string[] {
return readAll<{ skill_id: string }>(
"SELECT skill_id FROM agent_skill_assignments WHERE agent_id = ?",
`SELECT asa.skill_id
FROM agent_skill_assignments asa
JOIN agents a ON a.id = asa.agent_id
JOIN company_skills cs ON cs.id = asa.skill_id
WHERE asa.agent_id = ?
AND a.company_id = ?
AND cs.company_id = ?
ORDER BY cs.created_at ASC`,
agentId,
companyId,
companyId,
).map((row) => row.skill_id);
},
listSkillAgentAssignments(skillId: string): string[] {
listSkillAgentAssignments(companyId: string, skillId: string): string[] {
return readAll<{ agent_id: string }>(
"SELECT agent_id FROM agent_skill_assignments WHERE skill_id = ?",
`SELECT asa.agent_id
FROM agent_skill_assignments asa
JOIN agents a ON a.id = asa.agent_id
JOIN company_skills cs ON cs.id = asa.skill_id
WHERE asa.skill_id = ?
AND a.company_id = ?
AND cs.company_id = ?
ORDER BY a.created_at ASC`,
skillId,
companyId,
companyId,
).map((row) => row.agent_id);
},
+8 -2
View File
@@ -168,7 +168,10 @@ export function registerSkillHandlers({
const parsed = listSkillAgentsSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
return ok(db.core.skills.listSkillAgentAssignments(parsed.skillId));
if (!db.core.skills.getCompanySkill(parsed.companyId, parsed.skillId)) {
return fail("not_found", `Skill ${parsed.skillId} not found`);
}
return ok(db.core.skills.listSkillAgentAssignments(parsed.companyId, parsed.skillId));
});
// ---- List skill IDs assigned to an agent ----
@@ -176,6 +179,9 @@ export function registerSkillHandlers({
const parsed = listAgentSkillsSchema.parse(payload);
const companyError = ensureCompanyExists(parsed.companyId);
if (companyError) return companyError;
return ok(db.core.skills.listAgentSkillAssignments(parsed.agentId));
if (!db.core.settings.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
return fail("not_found", `Agent ${parsed.agentId} not found`);
}
return ok(db.core.skills.listAgentSkillAssignments(parsed.companyId, parsed.agentId));
});
}
+4 -3
View File
@@ -72,7 +72,7 @@ export function autoAssignSkillsForHire(
return { assignedSkillIds: [] };
}
const alreadyAssigned = new Set(db.core.skills.listAgentSkillAssignments(input.agentId));
const alreadyAssigned = new Set(db.core.skills.listAgentSkillAssignments(input.companyId, input.agentId));
const wanted = new Set(categories);
const catalogue = db.core.skills.listCompanySkills(input.companyId);
@@ -81,8 +81,9 @@ export function autoAssignSkillsForHire(
if (alreadyAssigned.has(skill.id)) continue;
const category = readBuiltinCategory(skill.metadata);
if (!category || !wanted.has(category)) continue;
db.core.skills.assignSkillToAgent(input.agentId, skill.id);
assigned.push(skill.id);
if (db.core.skills.assignSkillToAgent(input.companyId, input.agentId, skill.id)) {
assigned.push(skill.id);
}
}
return { assignedSkillIds: assigned };
+7 -3
View File
@@ -58,16 +58,20 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl
): DesktopResult<void> {
const skill = db.core.skills.getCompanySkill(companyId, skillId);
if (!skill) return fail("not_found", `Skill ${skillId} not found for company`);
db.core.skills.assignSkillToAgent(agentId, skillId);
if (!db.core.skills.assignSkillToAgent(companyId, agentId, skillId)) {
return fail("not_found", `Agent ${agentId} not found for company`);
}
return ok(undefined);
}
function removeFromAgent(
_companyId: string,
companyId: string,
agentId: string,
skillId: string,
): DesktopResult<void> {
db.core.skills.removeSkillFromAgent(agentId, skillId);
if (!db.core.skills.removeSkillFromAgent(companyId, agentId, skillId)) {
return fail("not_found", `Skill assignment not found for company`);
}
return ok(undefined);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function createSkillRuntime({ db, logger, skillsRoot, skillFilesDir, impo
agentId: string,
_connectorId: string,
): ProjectedSkill[] {
const assignedIds = new Set(db.core.skills.listAgentSkillAssignments(agentId));
const assignedIds = new Set(db.core.skills.listAgentSkillAssignments(companyId, agentId));
if (assignedIds.size === 0) return [];
const companySkills = db.core.skills.listCompanySkills(companyId);
const skillPaths = companySkills
+2 -2
View File
@@ -30,7 +30,7 @@ interface CapabilityMatchDb {
getAgent(id: string): { name: string; capabilities: string };
};
skills: {
listAgentSkillAssignments(id: string): string[];
listAgentSkillAssignments(companyId: string, agentId: string): string[];
getCompanySkill(companyId: string, skillId: string): { name: string; description: string | null; slug: string } | null;
};
};
@@ -139,7 +139,7 @@ export function checkCapabilityMatch(
const capabilityOverlap = taskWords.filter((w) => capWords.includes(w)).length;
let skillOverlap = 0;
const skillIds = db.core.skills.listAgentSkillAssignments(agentId);
const skillIds = db.core.skills.listAgentSkillAssignments(companyId, agentId);
for (const skillId of skillIds) {
const skill = db.core.skills.getCompanySkill(companyId, skillId);
if (skill) {
@@ -92,7 +92,7 @@ describe("skill-aware capability registry", () => {
createdAt: now,
updatedAt: now,
});
db.core.skills.assignSkillToAgent(styledEngineerId, skillId);
db.core.skills.assignSkillToAgent(companyId, styledEngineerId, skillId);
const taskId = db.core.tasks.saveTask({
companyId,