Expose agent task assignment permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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