diff --git a/src/main/api-server/routes-agent-control.ts b/src/main/api-server/routes-agent-control.ts index 26778a9d..dee49f97 100644 --- a/src/main/api-server/routes-agent-control.ts +++ b/src/main/api-server/routes-agent-control.ts @@ -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 => 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}` }); }); diff --git a/src/main/database/skills.ts b/src/main/database/skills.ts index ec926c80..02710955 100644 --- a/src/main/database/skills.ts +++ b/src/main/database/skills.ts @@ -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); }, diff --git a/src/main/ipc-skill-handlers.ts b/src/main/ipc-skill-handlers.ts index c3b96b3e..061c93cf 100644 --- a/src/main/ipc-skill-handlers.ts +++ b/src/main/ipc-skill-handlers.ts @@ -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)); }); } diff --git a/src/main/skills/auto-assign-on-hire.ts b/src/main/skills/auto-assign-on-hire.ts index 151333f2..9a2b39c6 100644 --- a/src/main/skills/auto-assign-on-hire.ts +++ b/src/main/skills/auto-assign-on-hire.ts @@ -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 }; diff --git a/src/main/skills/crud.ts b/src/main/skills/crud.ts index 65767c70..e84c9b53 100644 --- a/src/main/skills/crud.ts +++ b/src/main/skills/crud.ts @@ -58,16 +58,20 @@ export function createSkillCrud({ db, logger, cleanupSkillFiles, parseGitHubUrl ): DesktopResult { 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 { - 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); } diff --git a/src/main/skills/runtime.ts b/src/main/skills/runtime.ts index e1397598..abaf54f5 100644 --- a/src/main/skills/runtime.ts +++ b/src/main/skills/runtime.ts @@ -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 diff --git a/src/main/task-quality-gates.ts b/src/main/task-quality-gates.ts index fb594ee1..dfeb9565 100644 --- a/src/main/task-quality-gates.ts +++ b/src/main/task-quality-gates.ts @@ -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) { diff --git a/tests/integration/skill-aware-assignment.spec.ts b/tests/integration/skill-aware-assignment.spec.ts index 7d508dc9..bc98dd53 100644 --- a/tests/integration/skill-aware-assignment.spec.ts +++ b/tests/integration/skill-aware-assignment.spec.ts @@ -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,