import { useCallback, useEffect, useMemo, useState } from "react"; import { useGateways } from "@/app/di"; import type { Agent, Capability, PermissionEffect, PermissionPosture, PermissionRule, PermissionSet, ProjectPermissions, } from "@/domain"; export type CapabilityChoice = "none" | PermissionEffect; export interface PolicyDraft { fallback: PermissionPosture; read: CapabilityChoice; write: CapabilityChoice; delete: CapabilityChoice; executeBash: CapabilityChoice; } export interface AgentPermissionRow { agent: Agent; override: PermissionSet | null; } export interface PermissionsViewModel { agents: Agent[]; rows: AgentPermissionRow[]; document: ProjectPermissions | null; projectDraft: PolicyDraft; busy: boolean; error: string | null; refresh: () => Promise; saveProjectDefaults: (draft: PolicyDraft) => Promise; clearProjectDefaults: () => Promise; saveAgentOverride: (agentId: string, draft: PolicyDraft) => Promise; clearAgentOverride: (agentId: string) => Promise; } const DEFAULT_DRAFT: PolicyDraft = { fallback: "ask", read: "none", write: "none", delete: "none", executeBash: "none", }; const CAPABILITIES: Capability[] = ["read", "write", "delete", "executeBash"]; function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as { message: unknown }).message); } return String(e); } export function draftFromSet(set?: PermissionSet | null): PolicyDraft { if (!set) return { ...DEFAULT_DRAFT }; const draft: PolicyDraft = { ...DEFAULT_DRAFT, fallback: set.fallback, }; for (const capability of CAPABILITIES) { draft[capability] = ruleChoice(set.rules, capability); } return draft; } function ruleChoice( rules: PermissionRule[], capability: Capability, ): CapabilityChoice { const matching = rules.filter((rule) => rule.capability === capability); if (matching.some((rule) => rule.effect === "deny")) return "deny"; if (matching.some((rule) => rule.effect === "allow")) return "allow"; return "none"; } export function setFromDraft(draft: PolicyDraft): PermissionSet { const rules: PermissionRule[] = []; for (const capability of CAPABILITIES) { const effect = draft[capability]; if (effect === "none") continue; rules.push(ruleFromChoice(capability, effect)); } return { fallback: draft.fallback, rules }; } function ruleFromChoice( capability: Capability, effect: PermissionEffect, ): PermissionRule { if (capability === "executeBash") { return { capability, effect, commands: [] }; } return { capability, effect, paths: ["**"] }; } export function isCustomPolicy(set?: PermissionSet | null): boolean { if (!set) return false; return set.rules.some((rule) => { if (rule.capability === "executeBash") { return (rule.commands ?? []).length > 0; } const paths = rule.paths ?? []; return paths.length !== 1 || paths[0] !== "**"; }); } export function usePermissions(projectId: string): PermissionsViewModel { const { agent, permission } = useGateways(); const [agents, setAgents] = useState([]); const [document, setDocument] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const refresh = useCallback(async () => { setBusy(true); setError(null); try { const [agentList, permissionDoc] = await Promise.all([ agent.listAgents(projectId), permission.getProjectPermissions(projectId), ]); setAgents(agentList); setDocument(permissionDoc); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [agent, permission, projectId]); useEffect(() => { void refresh(); }, [refresh]); const rows = useMemo(() => { const overrides = new Map( (document?.agents ?? []).map((entry) => [entry.agentId, entry.permissions]), ); return agents.map((candidate) => ({ agent: candidate, override: overrides.get(candidate.id) ?? null, })); }, [agents, document]); const projectDraft = useMemo( () => draftFromSet(document?.projectDefaults ?? null), [document], ); const saveProjectDefaults = useCallback( async (draft: PolicyDraft) => { setBusy(true); setError(null); try { setDocument( await permission.updateProjectPermissions(projectId, setFromDraft(draft)), ); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [permission, projectId], ); const clearProjectDefaults = useCallback(async () => { setBusy(true); setError(null); try { setDocument(await permission.updateProjectPermissions(projectId, null)); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [permission, projectId]); const saveAgentOverride = useCallback( async (agentId: string, draft: PolicyDraft) => { setBusy(true); setError(null); try { setDocument( await permission.updateAgentPermissions( projectId, agentId, setFromDraft(draft), ), ); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [permission, projectId], ); const clearAgentOverride = useCallback( async (agentId: string) => { setBusy(true); setError(null); try { setDocument( await permission.updateAgentPermissions(projectId, agentId, null), ); } catch (e) { setError(describe(e)); } finally { setBusy(false); } }, [permission, projectId], ); return { agents, rows, document, projectDraft, busy, error, refresh, saveProjectDefaults, clearProjectDefaults, saveAgentOverride, clearAgentOverride, }; }