Expose agent task assignment permissions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-19 08:14:29 -05:00
parent bcc1d9f3d6
commit f9d685344d
10 changed files with 310 additions and 27 deletions

View File

@@ -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>