Expose agent task assignment permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -123,6 +123,9 @@ export type {
|
||||
InstanceExperimentalSettings,
|
||||
InstanceSettings,
|
||||
Agent,
|
||||
AgentAccessState,
|
||||
AgentChainOfCommandEntry,
|
||||
AgentDetail,
|
||||
AgentPermissions,
|
||||
AgentKeyCreated,
|
||||
AgentConfigRevision,
|
||||
|
||||
@@ -4,11 +4,29 @@ import type {
|
||||
AgentRole,
|
||||
AgentStatus,
|
||||
} from "../constants.js";
|
||||
import type {
|
||||
CompanyMembership,
|
||||
PrincipalPermissionGrant,
|
||||
} from "./access.js";
|
||||
|
||||
export interface AgentPermissions {
|
||||
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 {
|
||||
id: string;
|
||||
companyId: string;
|
||||
@@ -34,6 +52,11 @@ export interface Agent {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AgentDetail extends Agent {
|
||||
chainOfCommand: AgentChainOfCommandEntry[];
|
||||
access: AgentAccessState;
|
||||
}
|
||||
|
||||
export interface AgentKeyCreated {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -2,6 +2,9 @@ export type { Company } from "./company.js";
|
||||
export type { InstanceExperimentalSettings, InstanceSettings } from "./instance.js";
|
||||
export type {
|
||||
Agent,
|
||||
AgentAccessState,
|
||||
AgentChainOfCommandEntry,
|
||||
AgentDetail,
|
||||
AgentPermissions,
|
||||
AgentKeyCreated,
|
||||
AgentConfigRevision,
|
||||
|
||||
@@ -100,6 +100,7 @@ export type TestAdapterEnvironment = z.infer<typeof testAdapterEnvironmentSchema
|
||||
|
||||
export const updateAgentPermissionsSchema = z.object({
|
||||
canCreateAgents: z.boolean(),
|
||||
canAssignTasks: z.boolean(),
|
||||
});
|
||||
|
||||
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;
|
||||
|
||||
@@ -2450,6 +2450,14 @@ export function accessRoutes(
|
||||
"member",
|
||||
"active"
|
||||
);
|
||||
await access.setPrincipalPermission(
|
||||
companyId,
|
||||
"agent",
|
||||
created.id,
|
||||
"tasks:assign",
|
||||
true,
|
||||
req.actor.userId ?? null
|
||||
);
|
||||
const grants = grantsFromDefaults(
|
||||
invite.defaultsPayload as Record<string, unknown> | null,
|
||||
"agent"
|
||||
|
||||
@@ -71,6 +71,80 @@ export function agentRoutes(db: Db) {
|
||||
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) {
|
||||
assertCompanyAccess(req, companyId);
|
||||
if (req.actor.type === "board") {
|
||||
@@ -575,8 +649,7 @@ export function agentRoutes(db: Db) {
|
||||
res.status(404).json({ error: "Agent not found" });
|
||||
return;
|
||||
}
|
||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
||||
res.json({ ...agent, chainOfCommand });
|
||||
res.json(await buildAgentDetail(agent));
|
||||
});
|
||||
|
||||
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) {
|
||||
const canRead = await actorCanReadConfigurationsForCompany(req, agent.companyId);
|
||||
if (!canRead) {
|
||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
||||
res.json({ ...redactForRestrictedAgentView(agent), chainOfCommand });
|
||||
res.json(await buildAgentDetail(agent, { restricted: true }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const chainOfCommand = await svc.getChainOfCommand(agent.id);
|
||||
res.json({ ...agent, chainOfCommand });
|
||||
res.json(await buildAgentDetail(agent));
|
||||
});
|
||||
|
||||
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) {
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
@@ -945,6 +1022,12 @@ export function agentRoutes(db: Db) {
|
||||
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) {
|
||||
await budgets.upsertPolicy(
|
||||
companyId,
|
||||
@@ -988,6 +1071,18 @@ export function agentRoutes(db: Db) {
|
||||
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);
|
||||
await logActivity(db, {
|
||||
companyId: agent.companyId,
|
||||
@@ -998,10 +1093,13 @@ export function agentRoutes(db: Db) {
|
||||
action: "agent.permissions_updated",
|
||||
entityType: "agent",
|
||||
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) => {
|
||||
|
||||
@@ -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 {
|
||||
isInstanceAdmin,
|
||||
canUser,
|
||||
@@ -264,5 +344,7 @@ export function accessService(db: Db) {
|
||||
listUserCompanyAccess,
|
||||
setUserCompanyAccess,
|
||||
setPrincipalGrants,
|
||||
listPrincipalGrants,
|
||||
setPrincipalPermission,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -955,6 +955,15 @@ export function companyPortabilityService(db: Db) {
|
||||
}
|
||||
|
||||
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);
|
||||
existingSlugToAgentId.set(normalizeAgentUrlKey(created.name) ?? created.id, created.id);
|
||||
resultAgents.push({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
Agent,
|
||||
AgentDetail,
|
||||
AdapterEnvironmentTestResult,
|
||||
AgentKeyCreated,
|
||||
AgentRuntimeState,
|
||||
@@ -45,6 +46,11 @@ export interface AgentHireResponse {
|
||||
approval: Approval | null;
|
||||
}
|
||||
|
||||
export interface AgentPermissionUpdate {
|
||||
canCreateAgents: boolean;
|
||||
canAssignTasks: boolean;
|
||||
}
|
||||
|
||||
function withCompanyScope(path: string, companyId?: string) {
|
||||
if (!companyId) return path;
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
@@ -62,7 +68,7 @@ export const agentsApi = {
|
||||
api.get<Record<string, unknown>[]>(`/companies/${companyId}/agent-configurations`),
|
||||
get: async (id: string, companyId?: string) => {
|
||||
try {
|
||||
return await api.get<Agent>(agentPath(id, companyId));
|
||||
return await api.get<AgentDetail>(agentPath(id, companyId));
|
||||
} catch (error) {
|
||||
// Backward-compat fallback: if backend shortname lookup reports ambiguity,
|
||||
// resolve using company agent list while ignoring terminated agents.
|
||||
@@ -83,7 +89,7 @@ export const agentsApi = {
|
||||
(agent) => agent.status !== "terminated" && normalizeAgentUrlKey(agent.urlKey) === urlKey,
|
||||
);
|
||||
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) =>
|
||||
@@ -100,8 +106,8 @@ export const agentsApi = {
|
||||
api.post<AgentHireResponse>(`/companies/${companyId}/agent-hires`, data),
|
||||
update: (id: string, data: Record<string, unknown>, companyId?: string) =>
|
||||
api.patch<Agent>(agentPath(id, companyId), data),
|
||||
updatePermissions: (id: string, data: { canCreateAgents: boolean }, companyId?: string) =>
|
||||
api.patch<Agent>(agentPath(id, companyId, "/permissions"), data),
|
||||
updatePermissions: (id: string, data: AgentPermissionUpdate, companyId?: string) =>
|
||||
api.patch<AgentDetail>(agentPath(id, companyId, "/permissions"), data),
|
||||
pause: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/pause"), {}),
|
||||
resume: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/resume"), {}),
|
||||
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 { useParams, useNavigate, Link, Navigate, useBeforeUnload } from "@/lib/router";
|
||||
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 { heartbeatsApi } from "../api/heartbeats";
|
||||
import { ApiError } from "../api/client";
|
||||
@@ -64,6 +70,7 @@ import { RunTranscriptView, type TranscriptMode } from "../components/transcript
|
||||
import {
|
||||
isUuidLike,
|
||||
type Agent,
|
||||
type AgentDetail as AgentDetailRecord,
|
||||
type BudgetPolicySummary,
|
||||
type HeartbeatRun,
|
||||
type HeartbeatRunEvent,
|
||||
@@ -486,7 +493,7 @@ export function AgentDetail() {
|
||||
const setSaveConfigAction = useCallback((fn: (() => void) | null) => { saveConfigActionRef.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],
|
||||
queryFn: () => agentsApi.get(routeAgentRef, lookupCompanyId),
|
||||
enabled: canFetchAgent,
|
||||
@@ -672,8 +679,8 @@ export function AgentDetail() {
|
||||
});
|
||||
|
||||
const updatePermissions = useMutation({
|
||||
mutationFn: (canCreateAgents: boolean) =>
|
||||
agentsApi.updatePermissions(agentLookupRef, { canCreateAgents }, resolvedCompanyId ?? undefined),
|
||||
mutationFn: (permissions: AgentPermissionUpdate) =>
|
||||
agentsApi.updatePermissions(agentLookupRef, permissions, resolvedCompanyId ?? undefined),
|
||||
onSuccess: () => {
|
||||
setActionError(null);
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) });
|
||||
@@ -1076,7 +1083,7 @@ function AgentOverview({
|
||||
agentId,
|
||||
agentRouteId,
|
||||
}: {
|
||||
agent: Agent;
|
||||
agent: AgentDetailRecord;
|
||||
runs: HeartbeatRun[];
|
||||
assignedIssues: { id: string; title: string; status: string; priority: string; identifier?: string | null; createdAt: Date }[];
|
||||
runtimeState?: AgentRuntimeState;
|
||||
@@ -1233,14 +1240,14 @@ function AgentConfigurePage({
|
||||
onSavingChange,
|
||||
updatePermissions,
|
||||
}: {
|
||||
agent: Agent;
|
||||
agent: AgentDetailRecord;
|
||||
agentId: string;
|
||||
companyId?: string;
|
||||
onDirtyChange: (dirty: boolean) => void;
|
||||
onSaveActionChange: (save: (() => void) | null) => void;
|
||||
onCancelActionChange: (cancel: (() => void) | null) => void;
|
||||
onSavingChange: (saving: boolean) => void;
|
||||
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
||||
updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean };
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [revisionsOpen, setRevisionsOpen] = useState(false);
|
||||
@@ -1340,13 +1347,13 @@ function ConfigurationTab({
|
||||
onSavingChange,
|
||||
updatePermissions,
|
||||
}: {
|
||||
agent: Agent;
|
||||
agent: AgentDetailRecord;
|
||||
companyId?: string;
|
||||
onDirtyChange: (dirty: boolean) => void;
|
||||
onSaveActionChange: (save: (() => void) | null) => void;
|
||||
onCancelActionChange: (cancel: (() => void) | null) => void;
|
||||
onSavingChange: (saving: boolean) => void;
|
||||
updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean };
|
||||
updatePermissions: { mutate: (permissions: AgentPermissionUpdate) => void; isPending: boolean };
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false);
|
||||
@@ -1389,6 +1396,19 @@ function ConfigurationTab({
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<AgentConfigForm
|
||||
@@ -1406,19 +1426,49 @@ function ConfigurationTab({
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">Permissions</h3>
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Can create new agents</span>
|
||||
<div className="border border-border rounded-lg p-4 space-y-4">
|
||||
<div className="flex items-center justify-between gap-4 text-sm">
|
||||
<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
|
||||
variant={agent.permissions?.canCreateAgents ? "default" : "outline"}
|
||||
variant={canCreateAgents ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
onClick={() =>
|
||||
updatePermissions.mutate(!Boolean(agent.permissions?.canCreateAgents))
|
||||
updatePermissions.mutate({
|
||||
canCreateAgents: !canCreateAgents,
|
||||
canAssignTasks: !canCreateAgents ? true : canAssignTasks,
|
||||
})
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user