fix(security): scope skill assignments by company
This commit is contained in:
1 parent
41e1dec647
commit
d1a3d23c70
8 files changed
+85
-22
No files matched your search
@@ -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}` });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user