Expose agent task assignment permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
bcc1d9f3d6
commit
f9d685344d
10 changed files with 310 additions and 27 deletions
|
|
@ -123,6 +123,9 @@ export type {
|
||||||
InstanceExperimentalSettings,
|
InstanceExperimentalSettings,
|
||||||
InstanceSettings,
|
InstanceSettings,
|
||||||
Agent,
|
Agent,
|
||||||
|
AgentAccessState,
|
||||||
|
AgentChainOfCommandEntry,
|
||||||
|
AgentDetail,
|
||||||
AgentPermissions,
|
AgentPermissions,
|
||||||
AgentKeyCreated,
|
AgentKeyCreated,
|
||||||
AgentConfigRevision,
|
AgentConfigRevision,
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,29 @@ import type {
|
||||||
AgentRole,
|
AgentRole,
|
||||||
AgentStatus,
|
AgentStatus,
|
||||||
} from "../constants.js";
|
} from "../constants.js";
|
||||||
|
import type {
|
||||||
|
CompanyMembership,
|
||||||
|
PrincipalPermissionGrant,
|
||||||
|
} from "./access.js";
|
||||||
|
|
||||||
export interface AgentPermissions {
|
export interface AgentPermissions {
|
||||||
canCreateAgents: boolean;
|
canCreateAgents: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentAccessState {
|
||||||
|
canAssignTasks: boolean;
|
||||||
|
taskAssignSource: "explicit_grant" | "agent_creator" | "ceo_role" | "none";
|
||||||
|
membership: CompanyMembership | null;
|
||||||
|
grants: PrincipalPermissionGrant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentChainOfCommandEntry {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: AgentRole;
|
||||||
|
title: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
|
@ -34,6 +52,11 @@ export interface Agent {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentDetail extends Agent {
|
||||||
|
chainOfCommand: AgentChainOfCommandEntry[];
|
||||||
|
access: AgentAccessState;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentKeyCreated {
|
export interface AgentKeyCreated {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ export type { Company } from "./company.js";
|
||||||
export type { InstanceExperimentalSettings, InstanceSettings } from "./instance.js";
|
export type { InstanceExperimentalSettings, InstanceSettings } from "./instance.js";
|
||||||
export type {
|
export type {
|
||||||
Agent,
|
Agent,
|
||||||
|
AgentAccessState,
|
||||||
|
AgentChainOfCommandEntry,
|
||||||
|
AgentDetail,
|
||||||
AgentPermissions,
|
AgentPermissions,
|
||||||
AgentKeyCreated,
|
AgentKeyCreated,
|
||||||
AgentConfigRevision,
|
AgentConfigRevision,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ export type TestAdapterEnvironment = z.infer<typeof testAdapterEnvironmentSchema
|
||||||
|
|
||||||
export const updateAgentPermissionsSchema = z.object({
|
export const updateAgentPermissionsSchema = z.object({
|
||||||
canCreateAgents: z.boolean(),
|
canCreateAgents: z.boolean(),
|
||||||
|
canAssignTasks: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;
|
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;
|
||||||
|
|
|
||||||
|
|
@ -2450,6 +2450,14 @@ export function accessRoutes(
|
||||||
"member",
|
"member",
|
||||||
"active"
|
"active"
|
||||||
);
|
);
|
||||||
|
await access.setPrincipalPermission(
|
||||||
|
companyId,
|
||||||
|
"agent",
|
||||||
|
created.id,
|
||||||
|
"tasks:assign",
|
||||||
|
true,
|
||||||
|
req.actor.userId ?? null
|
||||||
|
);
|
||||||
const grants = grantsFromDefaults(
|
const grants = grantsFromDefaults(
|
||||||
invite.defaultsPayload as Record<string, unknown> | null,
|
invite.defaultsPayload as Record<string, unknown> | null,
|
||||||
"agent"
|
"agent"
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,80 @@ export function agentRoutes(db: Db) {
|
||||||
return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
|
return Boolean((agent.permissions as Record<string, unknown>).canCreateAgents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function buildAgentAccessState(agent: NonNullable<Awaited<ReturnType<typeof svc.getById>>>) {
|
||||||
|
const membership = await access.getMembership(agent.companyId, "agent", agent.id);
|
||||||
|
const grants = membership
|
||||||
|
? await access.listPrincipalGrants(agent.companyId, "agent", agent.id)
|
||||||
|
: [];
|
||||||
|
const hasExplicitTaskAssignGrant = grants.some((grant) => grant.permissionKey === "tasks:assign");
|
||||||
|
|
||||||
|
if (agent.role === "ceo") {
|
||||||
|
return {
|
||||||
|
canAssignTasks: true,
|
||||||
|
taskAssignSource: "ceo_role" as const,
|
||||||
|
membership,
|
||||||
|
grants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canCreateAgents(agent)) {
|
||||||
|
return {
|
||||||
|
canAssignTasks: true,
|
||||||
|
taskAssignSource: "agent_creator" as const,
|
||||||
|
membership,
|
||||||
|
grants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasExplicitTaskAssignGrant) {
|
||||||
|
return {
|
||||||
|
canAssignTasks: true,
|
||||||
|
taskAssignSource: "explicit_grant" as const,
|
||||||
|
membership,
|
||||||
|
grants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canAssignTasks: false,
|
||||||
|
taskAssignSource: "none" as const,
|
||||||
|
membership,
|
||||||
|
grants,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAgentDetail(
|
||||||
|
agent: NonNullable<Awaited<ReturnType<typeof svc.getById>>>,
|
||||||
|
options?: { restricted?: boolean },
|
||||||
|
) {
|
||||||
|
const [chainOfCommand, accessState] = await Promise.all([
|
||||||
|
svc.getChainOfCommand(agent.id),
|
||||||
|
buildAgentAccessState(agent),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(options?.restricted ? redactForRestrictedAgentView(agent) : agent),
|
||||||
|
chainOfCommand,
|
||||||
|
access: accessState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyDefaultAgentTaskAssignGrant(
|
||||||
|
companyId: string,
|
||||||
|
agentId: string,
|
||||||
|
grantedByUserId: string | null,
|
||||||
|
) {
|
||||||
|
await access.ensureMembership(companyId, "agent", agentId, "member", "active");
|
||||||
|
await access.setPrincipalPermission(
|
||||||
|
companyId,
|
||||||
|
"agent",
|
||||||
|
agentId,
|
||||||
|
"tasks:assign",
|
||||||
|
true,
|
||||||
|
grantedByUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function assertCanCreateAgentsForCompany(req: Request, companyId: string) {
|
async function assertCanCreateAgentsForCompany(req: Request, companyId: string) {
|
||||||
assertCompanyAccess(req, companyId);
|
assertCompanyAccess(req, companyId);
|
||||||
if (req.actor.type === "board") {
|
if (req.actor.type === "board") {
|
||||||
|
|
@ -575,8 +649,7 @@ export function agentRoutes(db: Db) {
|
||||||
res.status(404).json({ error: "Agent not found" });
|
res.status(404).json({ error: "Agent not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
res.json(await buildAgentDetail(agent));
|
||||||
res.json({ ...agent, chainOfCommand });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/agents/me/inbox-lite", async (req, res) => {
|
router.get("/agents/me/inbox-lite", async (req, res) => {
|
||||||
|
|
@ -618,13 +691,11 @@ export function agentRoutes(db: Db) {
|
||||||
if (req.actor.type === "agent" && req.actor.agentId !== id) {
|
if (req.actor.type === "agent" && req.actor.agentId !== id) {
|
||||||
const canRead = await actorCanReadConfigurationsForCompany(req, agent.companyId);
|
const canRead = await actorCanReadConfigurationsForCompany(req, agent.companyId);
|
||||||
if (!canRead) {
|
if (!canRead) {
|
||||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
res.json(await buildAgentDetail(agent, { restricted: true }));
|
||||||
res.json({ ...redactForRestrictedAgentView(agent), chainOfCommand });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
res.json(await buildAgentDetail(agent));
|
||||||
res.json({ ...agent, chainOfCommand });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/agents/:id/configuration", async (req, res) => {
|
router.get("/agents/:id/configuration", async (req, res) => {
|
||||||
|
|
@ -884,6 +955,12 @@ export function agentRoutes(db: Db) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await applyDefaultAgentTaskAssignGrant(
|
||||||
|
companyId,
|
||||||
|
agent.id,
|
||||||
|
actor.actorType === "user" ? actor.actorId : null,
|
||||||
|
);
|
||||||
|
|
||||||
if (approval) {
|
if (approval) {
|
||||||
await logActivity(db, {
|
await logActivity(db, {
|
||||||
companyId,
|
companyId,
|
||||||
|
|
@ -945,6 +1022,12 @@ export function agentRoutes(db: Db) {
|
||||||
details: { name: agent.name, role: agent.role },
|
details: { name: agent.name, role: agent.role },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await applyDefaultAgentTaskAssignGrant(
|
||||||
|
companyId,
|
||||||
|
agent.id,
|
||||||
|
req.actor.type === "board" ? (req.actor.userId ?? null) : null,
|
||||||
|
);
|
||||||
|
|
||||||
if (agent.budgetMonthlyCents > 0) {
|
if (agent.budgetMonthlyCents > 0) {
|
||||||
await budgets.upsertPolicy(
|
await budgets.upsertPolicy(
|
||||||
companyId,
|
companyId,
|
||||||
|
|
@ -988,6 +1071,18 @@ export function agentRoutes(db: Db) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const effectiveCanAssignTasks =
|
||||||
|
agent.role === "ceo" || Boolean(agent.permissions?.canCreateAgents) || req.body.canAssignTasks;
|
||||||
|
await access.ensureMembership(agent.companyId, "agent", agent.id, "member", "active");
|
||||||
|
await access.setPrincipalPermission(
|
||||||
|
agent.companyId,
|
||||||
|
"agent",
|
||||||
|
agent.id,
|
||||||
|
"tasks:assign",
|
||||||
|
effectiveCanAssignTasks,
|
||||||
|
req.actor.type === "board" ? (req.actor.userId ?? null) : null,
|
||||||
|
);
|
||||||
|
|
||||||
const actor = getActorInfo(req);
|
const actor = getActorInfo(req);
|
||||||
await logActivity(db, {
|
await logActivity(db, {
|
||||||
companyId: agent.companyId,
|
companyId: agent.companyId,
|
||||||
|
|
@ -998,10 +1093,13 @@ export function agentRoutes(db: Db) {
|
||||||
action: "agent.permissions_updated",
|
action: "agent.permissions_updated",
|
||||||
entityType: "agent",
|
entityType: "agent",
|
||||||
entityId: agent.id,
|
entityId: agent.id,
|
||||||
details: req.body,
|
details: {
|
||||||
|
canCreateAgents: agent.permissions?.canCreateAgents ?? false,
|
||||||
|
canAssignTasks: effectiveCanAssignTasks,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json(agent);
|
res.json(await buildAgentDetail(agent));
|
||||||
});
|
});
|
||||||
|
|
||||||
router.patch("/agents/:id/instructions-path", validate(updateAgentInstructionsPathSchema), async (req, res) => {
|
router.patch("/agents/:id/instructions-path", validate(updateAgentInstructionsPathSchema), async (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,86 @@ export function accessService(db: Db) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listPrincipalGrants(
|
||||||
|
companyId: string,
|
||||||
|
principalType: PrincipalType,
|
||||||
|
principalId: string,
|
||||||
|
) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(principalPermissionGrants)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(principalPermissionGrants.companyId, companyId),
|
||||||
|
eq(principalPermissionGrants.principalType, principalType),
|
||||||
|
eq(principalPermissionGrants.principalId, principalId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(principalPermissionGrants.permissionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setPrincipalPermission(
|
||||||
|
companyId: string,
|
||||||
|
principalType: PrincipalType,
|
||||||
|
principalId: string,
|
||||||
|
permissionKey: PermissionKey,
|
||||||
|
enabled: boolean,
|
||||||
|
grantedByUserId: string | null,
|
||||||
|
scope: Record<string, unknown> | null = null,
|
||||||
|
) {
|
||||||
|
if (!enabled) {
|
||||||
|
await db
|
||||||
|
.delete(principalPermissionGrants)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(principalPermissionGrants.companyId, companyId),
|
||||||
|
eq(principalPermissionGrants.principalType, principalType),
|
||||||
|
eq(principalPermissionGrants.principalId, principalId),
|
||||||
|
eq(principalPermissionGrants.permissionKey, permissionKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureMembership(companyId, principalType, principalId, "member", "active");
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(principalPermissionGrants)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(principalPermissionGrants.companyId, companyId),
|
||||||
|
eq(principalPermissionGrants.principalType, principalType),
|
||||||
|
eq(principalPermissionGrants.principalId, principalId),
|
||||||
|
eq(principalPermissionGrants.permissionKey, permissionKey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((rows) => rows[0] ?? null);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await db
|
||||||
|
.update(principalPermissionGrants)
|
||||||
|
.set({
|
||||||
|
scope,
|
||||||
|
grantedByUserId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(principalPermissionGrants.id, existing.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(principalPermissionGrants).values({
|
||||||
|
companyId,
|
||||||
|
principalType,
|
||||||
|
principalId,
|
||||||
|
permissionKey,
|
||||||
|
scope,
|
||||||
|
grantedByUserId,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isInstanceAdmin,
|
isInstanceAdmin,
|
||||||
canUser,
|
canUser,
|
||||||
|
|
@ -264,5 +344,7 @@ export function accessService(db: Db) {
|
||||||
listUserCompanyAccess,
|
listUserCompanyAccess,
|
||||||
setUserCompanyAccess,
|
setUserCompanyAccess,
|
||||||
setPrincipalGrants,
|
setPrincipalGrants,
|
||||||
|
listPrincipalGrants,
|
||||||
|
setPrincipalPermission,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -955,6 +955,15 @@ export function companyPortabilityService(db: Db) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await agents.create(targetCompany.id, patch);
|
const created = await agents.create(targetCompany.id, patch);
|
||||||
|
await access.ensureMembership(targetCompany.id, "agent", created.id, "member", "active");
|
||||||
|
await access.setPrincipalPermission(
|
||||||
|
targetCompany.id,
|
||||||
|
"agent",
|
||||||
|
created.id,
|
||||||
|
"tasks:assign",
|
||||||
|
true,
|
||||||
|
actorUserId ?? null,
|
||||||
|
);
|
||||||
importedSlugToAgentId.set(planAgent.slug, created.id);
|
importedSlugToAgentId.set(planAgent.slug, created.id);
|
||||||
existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id);
|
existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id);
|
||||||
resultAgents.push({
|
resultAgents.push({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
Agent,
|
Agent,
|
||||||
|
AgentDetail,
|
||||||
AdapterEnvironmentTestResult,
|
AdapterEnvironmentTestResult,
|
||||||
AgentKeyCreated,
|
AgentKeyCreated,
|
||||||
AgentRuntimeState,
|
AgentRuntimeState,
|
||||||
|
|
@ -45,6 +46,11 @@ export interface AgentHireResponse {
|
||||||
approval: Approval | null;
|
approval: Approval | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentPermissionUpdate {
|
||||||
|
canCreateAgents: boolean;
|
||||||
|
canAssignTasks: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
function withCompanyScope(path: string, companyId?: string) {
|
function withCompanyScope(path: string, companyId?: string) {
|
||||||
if (!companyId) return path;
|
if (!companyId) return path;
|
||||||
const separator = path.includes("?") ? "&" : "?";
|
const separator = path.includes("?") ? "&" : "?";
|
||||||
|
|
@ -62,7 +68,7 @@ export const agentsApi = {
|
||||||
api.get<Record<string, unknown>[]>(`/companies/${companyId}/agent-configurations`),
|
api.get<Record<string, unknown>[]>(`/companies/${companyId}/agent-configurations`),
|
||||||
get: async (id: string, companyId?: string) => {
|
get: async (id: string, companyId?: string) => {
|
||||||
try {
|
try {
|
||||||
return await api.get<Agent>(agentPath(id, companyId));
|
return await api.get<AgentDetail>(agentPath(id, companyId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Backward-compat fallback: if backend shortname lookup reports ambiguity,
|
// Backward-compat fallback: if backend shortname lookup reports ambiguity,
|
||||||
// resolve using company agent list while ignoring terminated agents.
|
// resolve using company agent list while ignoring terminated agents.
|
||||||
|
|
@ -83,7 +89,7 @@ export const agentsApi = {
|
||||||
(agent) => agent.status !== "terminated" && normalizeAgentUrlKey(agent.urlKey) === urlKey,
|
(agent) => agent.status !== "terminated" && normalizeAgentUrlKey(agent.urlKey) === urlKey,
|
||||||
);
|
);
|
||||||
if (matches.length !== 1) throw error;
|
if (matches.length !== 1) throw error;
|
||||||
return api.get<Agent>(agentPath(matches[0]!.id, companyId));
|
return api.get<AgentDetail>(agentPath(matches[0]!.id, companyId));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getConfiguration: (id: string, companyId?: string) =>
|
getConfiguration: (id: string, companyId?: string) =>
|
||||||
|
|
@ -100,8 +106,8 @@ export const agentsApi = {
|
||||||
api.post<AgentHireResponse>(`/companies/${companyId}/agent-hires`, data),
|
api.post<AgentHireResponse>(`/companies/${companyId}/agent-hires`, data),
|
||||||
update: (id: string, data: Record<string, unknown>, companyId?: string) =>
|
update: (id: string, data: Record<string, unknown>, companyId?: string) =>
|
||||||
api.patch<Agent>(agentPath(id, companyId), data),
|
api.patch<Agent>(agentPath(id, companyId), data),
|
||||||
updatePermissions: (id: string, data: { canCreateAgents: boolean }, companyId?: string) =>
|
updatePermissions: (id: string, data: AgentPermissionUpdate, companyId?: string) =>
|
||||||
api.patch<Agent>(agentPath(id, companyId, "/permissions"), data),
|
api.patch<AgentDetail>(agentPath(id, companyId, "/permissions"), data),
|
||||||
pause: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/pause"), {}),
|
pause: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/pause"), {}),
|
||||||
resume: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/resume"), {}),
|
resume: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/resume"), {}),
|
||||||
terminate: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/terminate"), {}),
|
terminate: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/terminate"), {}),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||||
import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router";
|
import { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { agentsApi, type AgentKey, type ClaudeLoginResult, type AvailableSkill } from "../api/agents";
|
import {
|
||||||
|
agentsApi,
|
||||||
|
type AgentKey,
|
||||||
|
type ClaudeLoginResult,
|
||||||
|
type AvailableSkill,
|
||||||
|
type AgentPermissionUpdate,
|
||||||
|
} from "../api/agents";
|
||||||
import { budgetsApi } from "../api/budgets";
|
import { budgetsApi } from "../api/budgets";
|
||||||
import { heartbeatsApi } from "../api/heartbeats";
|
import { heartbeatsApi } from "../api/heartbeats";
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
|
|
@ -64,6 +70,7 @@ import { RunTranscriptView, type TranscriptMode } from "../components/transcript
|
||||||
import {
|
import {
|
||||||
isUuidLike,
|
isUuidLike,
|
||||||
type Agent,
|
type Agent,
|
||||||
|
type AgentDetail as AgentDetailRecord,
|
||||||
type BudgetPolicySummary,
|
type BudgetPolicySummary,
|
||||||
type HeartbeatRun,
|
type HeartbeatRun,
|
||||||
type HeartbeatRunEvent,
|
type HeartbeatRunEvent,
|
||||||
|
|
@ -486,7 +493,7 @@ export function AgentDetail() {
|
||||||
const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.current = fn; }, []);
|
const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.current = fn; }, []);
|
||||||
const setCancelConfigAction = useCallback((fn: (() => void) | null) => { cancelConfigActionRef.current = fn; }, []);
|
const setCancelConfigAction = useCallback((fn: (() => void) | null) => { cancelConfigActionRef.current = fn; }, []);
|
||||||
|
|
||||||
const { data: agent, isLoading, error } = useQuery({
|
const { data: agent, isLoading, error } = useQuery<AgentDetailRecord>({
|
||||||
queryKey: [...queryKeys.agents.detail(routeAgentRef), lookupCompanyId ?? null],
|
queryKey: [...queryKeys.agents.detail(routeAgentRef), lookupCompanyId ?? null],
|
||||||
queryFn: () => agentsApi.get(routeAgentRef, lookupCompanyId),
|
queryFn: () => agentsApi.get(routeAgentRef, lookupCompanyId),
|
||||||
enabled: canFetchAgent,
|
enabled: canFetchAgent,
|
||||||
|
|
@ -672,8 +679,8 @@ export function AgentDetail() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePermissions = useMutation({
|
const updatePermissions = useMutation({
|
||||||
mutationFn: (canCreateAgents: boolean) =>
|
mutationFn: (permissions: AgentPermissionUpdate) =>
|
||||||
agentsApi.updatePermissions(agentLookupRef, { canCreateAgents }, resolvedCompanyId ?? undefined),
|
agentsApi.updatePermissions(agentLookupRef, permissions, resolvedCompanyId ?? undefined),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setActionError(null);
|
setActionError(null);
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) });
|
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) });
|
||||||
|
|
@ -1076,7 +1083,7 @@ function AgentOverview({
|
||||||
agentId,
|
agentId,
|
||||||
agentRouteId,
|
agentRouteId,
|
||||||
}: {
|
}: {
|
||||||
agent: Agent;
|
agent: AgentDetailRecord;
|
||||||
runs: HeartbeatRun[];
|
runs: HeartbeatRun[];
|
||||||
assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[];
|
assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[];
|
||||||
runtimeState?: AgentRuntimeState;
|
runtimeState?: AgentRuntimeState;
|
||||||
|
|
@ -1233,14 +1240,14 @@ function AgentConfigurePage({
|
||||||
onSavingChange,
|
onSavingChange,
|
||||||
updatePermissions,
|
updatePermissions,
|
||||||
}: {
|
}: {
|
||||||
agent: Agent;
|
agent: AgentDetailRecord;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
onDirtyChange: (dirty: boolean) => void;
|
onDirtyChange: (dirty: boolean) => void;
|
||||||
onSaveActionChange: (save: (() => void) | null) => void;
|
onSaveActionChange: (save: (() => void) | null) => void;
|
||||||
onCancelActionChange: (cancel: (() => void) | null) => void;
|
onCancelActionChange: (cancel: (() => void) | null) => void;
|
||||||
onSavingChange: (saving: boolean) => void;
|
onSavingChange: (saving: boolean) => void;
|
||||||
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean };
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [revisionsOpen, setRevisionsOpen] = useState(false);
|
const [revisionsOpen, setRevisionsOpen] = useState(false);
|
||||||
|
|
@ -1340,13 +1347,13 @@ function ConfigurationTab({
|
||||||
onSavingChange,
|
onSavingChange,
|
||||||
updatePermissions,
|
updatePermissions,
|
||||||
}: {
|
}: {
|
||||||
agent: Agent;
|
agent: AgentDetailRecord;
|
||||||
companyId?: string;
|
companyId?: string;
|
||||||
onDirtyChange: (dirty: boolean) => void;
|
onDirtyChange: (dirty: boolean) => void;
|
||||||
onSaveActionChange: (save: (() => void) | null) => void;
|
onSaveActionChange: (save: (() => void) | null) => void;
|
||||||
onCancelActionChange: (cancel: (() => void) | null) => void;
|
onCancelActionChange: (cancel: (() => void) | null) => void;
|
||||||
onSavingChange: (saving: boolean) => void;
|
onSavingChange: (saving: boolean) => void;
|
||||||
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean };
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false);
|
const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false);
|
||||||
|
|
@ -1389,6 +1396,19 @@ function ConfigurationTab({
|
||||||
onSavingChange(isConfigSaving);
|
onSavingChange(isConfigSaving);
|
||||||
}, [onSavingChange, isConfigSaving]);
|
}, [onSavingChange, isConfigSaving]);
|
||||||
|
|
||||||
|
const canCreateAgents = Boolean(agent.permissions?.canCreateAgents);
|
||||||
|
const canAssignTasks = Boolean(agent.access?.canAssignTasks);
|
||||||
|
const taskAssignSource = agent.access?.taskAssignSource ?? "none";
|
||||||
|
const taskAssignLocked = agent.role === "ceo" || canCreateAgents;
|
||||||
|
const taskAssignHint =
|
||||||
|
taskAssignSource === "ceo_role"
|
||||||
|
? "Enabled automatically for CEO agents."
|
||||||
|
: taskAssignSource === "agent_creator"
|
||||||
|
? "Enabled automatically while this agent can create new agents."
|
||||||
|
: taskAssignSource === "explicit_grant"
|
||||||
|
? "Enabled via explicit company permission grant."
|
||||||
|
: "Disabled unless explicitly granted.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<AgentConfigForm
|
<AgentConfigForm
|
||||||
|
|
@ -1406,19 +1426,49 @@ function ConfigurationTab({
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium mb-3">Permissions</h3>
|
<h3 className="text-sm font-medium mb-3">Permissions</h3>
|
||||||
<div className="border border-border rounded-lg p-4">
|
<div className="border border-border rounded-lg p-4 space-y-4">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between gap-4 text-sm">
|
||||||
<span>Can create new agents</span>
|
<div className="space-y-1">
|
||||||
|
<div>Can create new agents</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Lets this agent create or hire agents and implicitly assign tasks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant={agent.permissions?.canCreateAgents ? "default" : "outline"}
|
variant={canCreateAgents ? "default" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 px-2.5 text-xs"
|
className="h-7 px-2.5 text-xs"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updatePermissions.mutate(!Boolean(agent.permissions?.canCreateAgents))
|
updatePermissions.mutate({
|
||||||
|
canCreateAgents: !canCreateAgents,
|
||||||
|
canAssignTasks: !canCreateAgents ? true : canAssignTasks,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
disabled={updatePermissions.isPending}
|
disabled={updatePermissions.isPending}
|
||||||
>
|
>
|
||||||
{agent.permissions?.canCreateAgents ? "Enabled" : "Disabled"}
|
{canCreateAgents ? "Enabled" : "Disabled"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 text-sm">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>Can assign tasks</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{taskAssignHint}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={canAssignTasks ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2.5 text-xs"
|
||||||
|
onClick={() =>
|
||||||
|
updatePermissions.mutate({
|
||||||
|
canCreateAgents,
|
||||||
|
canAssignTasks: !canAssignTasks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={updatePermissions.isPending || taskAssignLocked}
|
||||||
|
>
|
||||||
|
{canAssignTasks ? "Enabled" : "Disabled"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue