import { useState } from "react";import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react";import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";import * as Exit from "effect/Exit";import { trackEvent } from "../api/analytics";import { generateKeyBetween } from "fractional-indexing";import { ChevronDownIcon } from "lucide-react";import { PolicyId, matchPattern, isValidPattern, type Owner, type ToolPolicyAction,} from "@executor-js/sdk/shared"; import { createPolicyOptimistic, policiesOptimisticAtom, removePolicyOptimistic, updatePolicyOptimistic,} from "../api/atoms";import { policyWriteKeys } from "../api/reactivity-keys";import { ownerLabel, useOwnerDisplay } from "../api/owner-display";import { badgeVariants } from "../components/badge";import { cn } from "../lib/utils";import { POLICY_ACTION_LABEL, POLICY_ACTIONS_IN_ORDER, POLICY_BADGE_VARIANT,} from "../lib/policy-display";import { Button } from "../components/button";import { PageContainer, PageHeader } from "../components/page";import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,} from "../components/alert-dialog";import { CardStack, CardStackContent, CardStackEntry, CardStackEntryActions, CardStackEntryContent, CardStackEntryDescription, CardStackEntryTitle, CardStackHeader,} from "../components/card-stack";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "../components/dropdown-menu";import { Input } from "../components/input";import { Select, SelectContent, SelectItem, SelectPrimitiveTrigger, SelectTrigger, SelectValue,} from "../components/select";import { Label } from "../components/label";import { useExecutorDocumentTitle } from "../lib/document-title";import { ErrorState } from "../components/error-state";import { isAsyncResultLoading } from "../lib/async-result"; // Owner guardrail ordering: org rules are the outer guardrail (rank 0), user// rules are inner (rank 1). Mirrors server-side resolution where the most// restrictive matched action across owners wins.const ownerRank = (owner: Owner): number => (owner === "org" ? 0 : 1); // The two owners a policy can target.const POLICY_OWNERS: readonly { readonly owner: Owner; readonly label: string }[] = [ { owner: "org", label: "Workspace" }, { owner: "user", label: "Personal" },]; // ---------------------------------------------------------------------------// Sort comparator — owner rank, then fractional-indexing key, then id as a// stable tiebreak. Identical positions can briefly happen across racing// inserts; without the tiebreak the rendered order flips between refetches, and// `generateKeyBetween` would also throw if asked to insert "between" two equal// keys.// --------------------------------------------------------------------------- const comparePolicy = (posA: string, idA: string, posB: string, idB: string): number => { if (posA < posB) return -1; if (posA > posB) return 1; if (idA < idB) return -1; if (idA > idB) return 1; return 0;}; // Pattern matching + validation come from the SDK so the UI's "matches N tools"// preview and the add-policy validation use the EXACT same grammar the executor// enforces — including mid-segment wildcards (`integration.*.*.tool`), which the// connection-aware policy model now relies on. (Re-exported below for callers.)const matchesPattern = matchPattern; // ---------------------------------------------------------------------------// Add-policy form// --------------------------------------------------------------------------- function AddPolicyForm(props: { onSubmit: (input: { owner: Owner; pattern: string; action: ToolPolicyAction }) => void; owner: Owner; onOwnerChange: (owner: Owner) => void; busy: boolean;}) { const [pattern, setPattern] = useState(""); const [action, setAction] = useState<ToolPolicyAction>("require_approval"); const valid = isValidPattern(pattern); // Non-org hosts (local/desktop) have one local workspace. New local policies // are org-owned internally to match the v1->v2 migration. const ownerDisplay = useOwnerDisplay(); const ownerChoices = ownerDisplay.isSinglePlayerHost ? POLICY_OWNERS.filter((option) => option.owner === "org").map((option) => ({ ...option, label: "Local", })) : POLICY_OWNERS; return ( <form className="flex flex-col gap-3 rounded-xl border border-border bg-card px-5 py-4" onSubmit={(e) => { e.preventDefault(); if (!valid) return; props.onSubmit({ owner: props.owner, pattern, action }); setPattern(""); setAction("require_approval"); }} > <div className="flex flex-col gap-1"> <Label htmlFor="policy-pattern" className="text-xs font-medium text-foreground/80"> Pattern </Label> <Input id="policy-pattern" placeholder="vercel.dns.* or *" value={pattern} onChange={(e) => setPattern(e.target.value)} className="font-mono text-sm" /> <p className="text-xs text-muted-foreground"> Exact tool id, trailing wildcard, or <code className="font-mono">*</code> for every tool. Examples: <code className="font-mono">*</code>,{" "} <code className="font-mono">vercel.*</code>,{" "} <code className="font-mono">vercel.dns.*</code>,{" "} <code className="font-mono">vercel.dns.create</code>. </p> </div> <div className="flex flex-col gap-1"> <Label className="text-xs font-medium text-foreground/80">Action</Label> <Select value={action} onValueChange={(v) => setAction(v as ToolPolicyAction)}> <SelectTrigger className="w-full"> <SelectValue /> </SelectTrigger> <SelectContent> {POLICY_ACTIONS_IN_ORDER.map((a) => ( <SelectItem key={a} value={a}> {POLICY_ACTION_LABEL[a]} </SelectItem> ))} </SelectContent> </Select> </div> {ownerChoices.length > 1 ? ( <div className="flex flex-col gap-1"> <Label className="text-xs font-medium text-foreground/80">Applies to</Label> <Select value={props.owner} onValueChange={(value) => props.onOwnerChange(value as Owner)} > <SelectTrigger className="w-full"> <SelectValue /> </SelectTrigger> <SelectContent> {ownerChoices.map((option) => ( <SelectItem key={option.owner} value={option.owner}> {option.label} </SelectItem> ))} </SelectContent> </Select> </div> ) : null} <div className="flex items-center justify-end"> <Button type="submit" disabled={!valid || props.busy} size="sm"> Add policy </Button> </div> </form> );} // ---------------------------------------------------------------------------// Policy row// --------------------------------------------------------------------------- function PolicyRow(props: { policy: { id: string; owner: Owner; pattern: string; action: ToolPolicyAction; }; isFirst: boolean; isLast: boolean; onRemove: () => void; onChangeAction: (action: ToolPolicyAction) => void; onMoveUp: () => void; onMoveDown: () => void; showOwnerLabel: boolean;}) { return ( <CardStackEntry> <CardStackEntryContent> <CardStackEntryTitle className="flex items-center gap-2 font-mono text-sm"> <span className="truncate">{props.policy.pattern}</span> {props.showOwnerLabel ? ( <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-sans text-[10px] leading-none text-muted-foreground"> {ownerLabel(props.policy.owner)} </span> ) : null} </CardStackEntryTitle> </CardStackEntryContent> <CardStackEntryActions> <Select value={props.policy.action} onValueChange={(v) => props.onChangeAction(v as ToolPolicyAction)} > <SelectPrimitiveTrigger className={cn( badgeVariants({ variant: POLICY_BADGE_VARIANT[props.policy.action], }), "cursor-pointer pr-1.5 gap-1 transition-[opacity,box-shadow] hover:opacity-80 focus-visible:outline-none data-[state=open]:ring-2 data-[state=open]:ring-ring/50", )} > {POLICY_ACTION_LABEL[props.policy.action]} <ChevronDownIcon className="size-3 opacity-70" /> </SelectPrimitiveTrigger> <SelectContent position="popper" align="end"> {POLICY_ACTIONS_IN_ORDER.map((a) => ( <SelectItem key={a} value={a}> {POLICY_ACTION_LABEL[a]} </SelectItem> ))} </SelectContent> </Select> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="size-7 opacity-0 transition-opacity group-hover/card-stack-entry:opacity-100 group-focus-within/card-stack-entry:opacity-100 data-[state=open]:opacity-100" > <svg viewBox="0 0 16 16" className="size-3"> <circle cx="8" cy="3" r="1.2" fill="currentColor" /> <circle cx="8" cy="8" r="1.2" fill="currentColor" /> <circle cx="8" cy="13" r="1.2" fill="currentColor" /> </svg> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem disabled={props.isFirst} onClick={props.onMoveUp}> Move up </DropdownMenuItem> <DropdownMenuItem disabled={props.isLast} onClick={props.onMoveDown}> Move down </DropdownMenuItem> <DropdownMenuItem className="text-destructive focus:text-destructive text-sm" onClick={props.onRemove} > Remove </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </CardStackEntryActions> </CardStackEntry> );} // ---------------------------------------------------------------------------// Page// --------------------------------------------------------------------------- export function PoliciesPage() { useExecutorDocumentTitle("Policies"); const policies = useAtomValue(policiesOptimisticAtom); const refreshPolicies = useAtomRefresh(policiesOptimisticAtom); const doCreate = useAtomSet(createPolicyOptimistic, { mode: "promiseExit" }); const doUpdate = useAtomSet(updatePolicyOptimistic, { mode: "promiseExit" }); const doRemove = useAtomSet(removePolicyOptimistic, { mode: "promiseExit" }); const [busy, setBusy] = useState(false); const [removingPolicy, setRemovingPolicy] = useState<{ id: string; owner: Owner; pattern: string; } | null>(null); const ownerDisplay = useOwnerDisplay(); // Policies default to org/workspace. On local this is the hidden Local owner // that v1 local data migrates into. const [targetOwner, setTargetOwner] = useState<Owner>("org"); const handleCreate = async (input: { owner: Owner; pattern: string; action: ToolPolicyAction; }) => { setBusy(true); const exit = await doCreate({ payload: { owner: input.owner, pattern: input.pattern, action: input.action, }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_created", { action: input.action, owner: input.owner, success: Exit.isSuccess(exit), }); if (Exit.isFailure(exit)) { setBusy(false); return; } setBusy(false); }; const handleUpdate = async (policy: { id: string; owner: Owner }, action: ToolPolicyAction) => { const exit = await doUpdate({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner, action }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_action_changed", { action, owner: policy.owner, success: Exit.isSuccess(exit), }); }; const handleRemove = async (policy: { id: string; owner: Owner }) => { setRemovingPolicy(null); const exit = await doRemove({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_removed", { owner: policy.owner, success: Exit.isSuccess(exit) }); }; const handleMove = async ( policy: { id: string; owner: Owner }, position: string, direction: "up" | "down", ) => { const exit = await doUpdate({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner, position }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_reordered", { owner: policy.owner, direction, success: Exit.isSuccess(exit), }); }; return ( <PageContainer> <PageHeader title="Policies" description="Decide which tools run automatically, which ask for approval, and which are blocked. The most restrictive matching rule wins; blocked tools are hidden from agents." /> <div className="mb-8"> <AddPolicyForm onSubmit={handleCreate} owner={targetOwner} onOwnerChange={setTargetOwner} busy={busy} /> </div> {isAsyncResultLoading(policies) ? ( <div className="flex items-center gap-2 py-8"> <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" /> <p className="text-sm text-muted-foreground">Loading policies…</p> </div> ) : ( AsyncResult.match(policies, { onInitial: () => ( <div className="flex items-center gap-2 py-8"> <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" /> <p className="text-sm text-muted-foreground">Loading policies…</p> </div> ), onFailure: () => ( <ErrorState message="Failed to load policies" onRetry={refreshPolicies} /> ), onSuccess: ({ value }) => { // Sort by owner rank (org outer, user inner), then position (lex // order on fractional-indexing keys), tiebreaking on id so // identical positions don't swap on refetch and // `generateKeyBetween` never sees duplicate neighbor keys (which // would throw). Optimistic placeholders carry `position: ""` so // they sort to the top of their owner group. const sorted = [...value].sort((a, b) => { const ownerOrder = ownerRank(a.owner) - ownerRank(b.owner); return ownerOrder === 0 ? comparePolicy(a.position, a.id, b.position, b.id) : ownerOrder; }); // Reorder math runs against committed rows only — placeholder rows // (empty `position`) aren't valid keys for `generateKeyBetween` and // aren't reorderable until the server confirms. const committedForOwner = (owner: Owner) => sorted.filter((p) => p.owner === owner && p.position !== ""); const committedIndex = (id: string, owner: Owner): number => committedForOwner(owner).findIndex((p) => p.id === id); const positionAbove = (id: string, owner: Owner): string => { const committed = committedForOwner(owner); const j = committedIndex(id, owner); if (j <= 0) return generateKeyBetween(null, committed[0]!.position); return j === 1 ? generateKeyBetween(null, committed[0]!.position) : generateKeyBetween(committed[j - 2]!.position, committed[j - 1]!.position); }; const positionBelow = (id: string, owner: Owner): string => { const committed = committedForOwner(owner); const j = committedIndex(id, owner); if (j === -1 || j >= committed.length - 1) return generateKeyBetween(committed[committed.length - 1]!.position, null); return j === committed.length - 2 ? generateKeyBetween(committed[committed.length - 1]!.position, null) : generateKeyBetween(committed[j + 1]!.position, committed[j + 2]!.position); }; return ( <CardStack> <CardStackHeader>Active policies</CardStackHeader> <CardStackContent> {sorted.length === 0 ? ( <CardStackEntry> <CardStackEntryContent> <CardStackEntryDescription> No policies yet. Tools fall back to their plugin's default approval behavior. </CardStackEntryDescription> </CardStackEntryContent> </CardStackEntry> ) : ( sorted.map((p) => { const committed = committedForOwner(p.owner); const j = committedIndex(p.id, p.owner); // Pending placeholder or only one committed row → no // reorder affordance. const reorderable = j !== -1 && committed.length > 1; return ( <PolicyRow key={p.id} policy={{ id: p.id, owner: p.owner, pattern: p.pattern, action: p.action, }} isFirst={!reorderable || j === 0} isLast={!reorderable || j === committed.length - 1} showOwnerLabel={ownerDisplay.showOwnerLabels} onRemove={() => setRemovingPolicy({ id: p.id, owner: p.owner, pattern: p.pattern }) } onChangeAction={(action) => handleUpdate({ id: p.id, owner: p.owner }, action) } onMoveUp={() => handleMove( { id: p.id, owner: p.owner }, positionAbove(p.id, p.owner), "up", ) } onMoveDown={() => handleMove( { id: p.id, owner: p.owner }, positionBelow(p.id, p.owner), "down", ) }Lines 1–500 of 548. Load more
import { useState } from "react"; import { useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import * as Exit from "effect/Exit"; import { trackEvent } from "../api/analytics"; import { generateKeyBetween } from "fractional-indexing"; import { ChevronDownIcon } from "lucide-react"; import { PolicyId, matchPattern, isValidPattern, type Owner, type ToolPolicyAction, } from "@executor-js/sdk/shared"; import { createPolicyOptimistic, policiesOptimisticAtom, removePolicyOptimistic, updatePolicyOptimistic, } from "../api/atoms"; import { policyWriteKeys } from "../api/reactivity-keys"; import { ownerLabel, useOwnerDisplay } from "../api/owner-display"; import { badgeVariants } from "../components/badge"; import { cn } from "../lib/utils"; import { POLICY_ACTION_LABEL, POLICY_ACTIONS_IN_ORDER, POLICY_BADGE_VARIANT, } from "../lib/policy-display"; import { Button } from "../components/button"; import { PageContainer, PageHeader } from "../components/page"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../components/alert-dialog"; import { CardStack, CardStackContent, CardStackEntry, CardStackEntryActions, CardStackEntryContent, CardStackEntryDescription, CardStackEntryTitle, CardStackHeader, } from "../components/card-stack"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../components/dropdown-menu"; import { Input } from "../components/input"; import { Select, SelectContent, SelectItem, SelectPrimitiveTrigger, SelectTrigger, SelectValue, } from "../components/select"; import { Label } from "../components/label"; import { useExecutorDocumentTitle } from "../lib/document-title"; import { ErrorState } from "../components/error-state"; import { isAsyncResultLoading } from "../lib/async-result"; // Owner guardrail ordering: org rules are the outer guardrail (rank 0), user // rules are inner (rank 1). Mirrors server-side resolution where the most // restrictive matched action across owners wins. const ownerRank = (owner: Owner): number => (owner === "org" ? 0 : 1); // The two owners a policy can target. const POLICY_OWNERS: readonly { readonly owner: Owner; readonly label: string }[] = [ { owner: "org", label: "Workspace" }, { owner: "user", label: "Personal" }, ]; // --------------------------------------------------------------------------- // Sort comparator — owner rank, then fractional-indexing key, then id as a // stable tiebreak. Identical positions can briefly happen across racing // inserts; without the tiebreak the rendered order flips between refetches, and // `generateKeyBetween` would also throw if asked to insert "between" two equal // keys. // --------------------------------------------------------------------------- const comparePolicy = (posA: string, idA: string, posB: string, idB: string): number => { if (posA < posB) return -1; if (posA > posB) return 1; if (idA < idB) return -1; if (idA > idB) return 1; return 0; }; // Pattern matching + validation come from the SDK so the UI's "matches N tools" // preview and the add-policy validation use the EXACT same grammar the executor // enforces — including mid-segment wildcards (`integration.*.*.tool`), which the // connection-aware policy model now relies on. (Re-exported below for callers.) const matchesPattern = matchPattern; // --------------------------------------------------------------------------- // Add-policy form // --------------------------------------------------------------------------- function AddPolicyForm(props: { onSubmit: (input: { owner: Owner; pattern: string; action: ToolPolicyAction }) => void; owner: Owner; onOwnerChange: (owner: Owner) => void; busy: boolean; }) { const [pattern, setPattern] = useState(""); const [action, setAction] = useState<ToolPolicyAction>("require_approval"); const valid = isValidPattern(pattern); // Non-org hosts (local/desktop) have one local workspace. New local policies // are org-owned internally to match the v1->v2 migration. const ownerDisplay = useOwnerDisplay(); const ownerChoices = ownerDisplay.isSinglePlayerHost ? POLICY_OWNERS.filter((option) => option.owner === "org").map((option) => ({ ...option, label: "Local", })) : POLICY_OWNERS; return ( <form className="flex flex-col gap-3 rounded-xl border border-border bg-card px-5 py-4" onSubmit={(e) => { e.preventDefault(); if (!valid) return; props.onSubmit({ owner: props.owner, pattern, action }); setPattern(""); setAction("require_approval"); }} > <div className="flex flex-col gap-1"> <Label htmlFor="policy-pattern" className="text-xs font-medium text-foreground/80"> Pattern </Label> <Input id="policy-pattern" placeholder="vercel.dns.* or *" value={pattern} onChange={(e) => setPattern(e.target.value)} className="font-mono text-sm" /> <p className="text-xs text-muted-foreground"> Exact tool id, trailing wildcard, or <code className="font-mono">*</code> for every tool. Examples: <code className="font-mono">*</code>,{" "} <code className="font-mono">vercel.*</code>,{" "} <code className="font-mono">vercel.dns.*</code>,{" "} <code className="font-mono">vercel.dns.create</code>. </p> </div> <div className="flex flex-col gap-1"> <Label className="text-xs font-medium text-foreground/80">Action</Label> <Select value={action} onValueChange={(v) => setAction(v as ToolPolicyAction)}> <SelectTrigger className="w-full"> <SelectValue /> </SelectTrigger> <SelectContent> {POLICY_ACTIONS_IN_ORDER.map((a) => ( <SelectItem key={a} value={a}> {POLICY_ACTION_LABEL[a]} </SelectItem> ))} </SelectContent> </Select> </div> {ownerChoices.length > 1 ? ( <div className="flex flex-col gap-1"> <Label className="text-xs font-medium text-foreground/80">Applies to</Label> <Select value={props.owner} onValueChange={(value) => props.onOwnerChange(value as Owner)} > <SelectTrigger className="w-full"> <SelectValue /> </SelectTrigger> <SelectContent> {ownerChoices.map((option) => ( <SelectItem key={option.owner} value={option.owner}> {option.label} </SelectItem> ))} </SelectContent> </Select> </div> ) : null} <div className="flex items-center justify-end"> <Button type="submit" disabled={!valid || props.busy} size="sm"> Add policy </Button> </div> </form> ); } // --------------------------------------------------------------------------- // Policy row // --------------------------------------------------------------------------- function PolicyRow(props: { policy: { id: string; owner: Owner; pattern: string; action: ToolPolicyAction; }; isFirst: boolean; isLast: boolean; onRemove: () => void; onChangeAction: (action: ToolPolicyAction) => void; onMoveUp: () => void; onMoveDown: () => void; showOwnerLabel: boolean; }) { return ( <CardStackEntry> <CardStackEntryContent> <CardStackEntryTitle className="flex items-center gap-2 font-mono text-sm"> <span className="truncate">{props.policy.pattern}</span> {props.showOwnerLabel ? ( <span className="shrink-0 rounded border border-border px-1.5 py-0.5 font-sans text-[10px] leading-none text-muted-foreground"> {ownerLabel(props.policy.owner)} </span> ) : null} </CardStackEntryTitle> </CardStackEntryContent> <CardStackEntryActions> <Select value={props.policy.action} onValueChange={(v) => props.onChangeAction(v as ToolPolicyAction)} > <SelectPrimitiveTrigger className={cn( badgeVariants({ variant: POLICY_BADGE_VARIANT[props.policy.action], }), "cursor-pointer pr-1.5 gap-1 transition-[opacity,box-shadow] hover:opacity-80 focus-visible:outline-none data-[state=open]:ring-2 data-[state=open]:ring-ring/50", )} > {POLICY_ACTION_LABEL[props.policy.action]} <ChevronDownIcon className="size-3 opacity-70" /> </SelectPrimitiveTrigger> <SelectContent position="popper" align="end"> {POLICY_ACTIONS_IN_ORDER.map((a) => ( <SelectItem key={a} value={a}> {POLICY_ACTION_LABEL[a]} </SelectItem> ))} </SelectContent> </Select> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="size-7 opacity-0 transition-opacity group-hover/card-stack-entry:opacity-100 group-focus-within/card-stack-entry:opacity-100 data-[state=open]:opacity-100" > <svg viewBox="0 0 16 16" className="size-3"> <circle cx="8" cy="3" r="1.2" fill="currentColor" /> <circle cx="8" cy="8" r="1.2" fill="currentColor" /> <circle cx="8" cy="13" r="1.2" fill="currentColor" /> </svg> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem disabled={props.isFirst} onClick={props.onMoveUp}> Move up </DropdownMenuItem> <DropdownMenuItem disabled={props.isLast} onClick={props.onMoveDown}> Move down </DropdownMenuItem> <DropdownMenuItem className="text-destructive focus:text-destructive text-sm" onClick={props.onRemove} > Remove </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </CardStackEntryActions> </CardStackEntry> ); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function PoliciesPage() { useExecutorDocumentTitle("Policies"); const policies = useAtomValue(policiesOptimisticAtom); const refreshPolicies = useAtomRefresh(policiesOptimisticAtom); const doCreate = useAtomSet(createPolicyOptimistic, { mode: "promiseExit" }); const doUpdate = useAtomSet(updatePolicyOptimistic, { mode: "promiseExit" }); const doRemove = useAtomSet(removePolicyOptimistic, { mode: "promiseExit" }); const [busy, setBusy] = useState(false); const [removingPolicy, setRemovingPolicy] = useState<{ id: string; owner: Owner; pattern: string; } | null>(null); const ownerDisplay = useOwnerDisplay(); // Policies default to org/workspace. On local this is the hidden Local owner // that v1 local data migrates into. const [targetOwner, setTargetOwner] = useState<Owner>("org"); const handleCreate = async (input: { owner: Owner; pattern: string; action: ToolPolicyAction; }) => { setBusy(true); const exit = await doCreate({ payload: { owner: input.owner, pattern: input.pattern, action: input.action, }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_created", { action: input.action, owner: input.owner, success: Exit.isSuccess(exit), }); if (Exit.isFailure(exit)) { setBusy(false); return; } setBusy(false); }; const handleUpdate = async (policy: { id: string; owner: Owner }, action: ToolPolicyAction) => { const exit = await doUpdate({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner, action }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_action_changed", { action, owner: policy.owner, success: Exit.isSuccess(exit), }); }; const handleRemove = async (policy: { id: string; owner: Owner }) => { setRemovingPolicy(null); const exit = await doRemove({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_removed", { owner: policy.owner, success: Exit.isSuccess(exit) }); }; const handleMove = async ( policy: { id: string; owner: Owner }, position: string, direction: "up" | "down", ) => { const exit = await doUpdate({ params: { policyId: PolicyId.make(policy.id) }, payload: { owner: policy.owner, position }, reactivityKeys: policyWriteKeys, }); trackEvent("policy_reordered", { owner: policy.owner, direction, success: Exit.isSuccess(exit), }); }; return ( <PageContainer> <PageHeader title="Policies" description="Decide which tools run automatically, which ask for approval, and which are blocked. The most restrictive matching rule wins; blocked tools are hidden from agents." /> <div className="mb-8"> <AddPolicyForm onSubmit={handleCreate} owner={targetOwner} onOwnerChange={setTargetOwner} busy={busy} /> </div> {isAsyncResultLoading(policies) ? ( <div className="flex items-center gap-2 py-8"> <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" /> <p className="text-sm text-muted-foreground">Loading policies…</p> </div> ) : ( AsyncResult.match(policies, { onInitial: () => ( <div className="flex items-center gap-2 py-8"> <div className="size-1.5 animate-pulse rounded-full bg-muted-foreground/30" /> <p className="text-sm text-muted-foreground">Loading policies…</p> </div> ), onFailure: () => ( <ErrorState message="Failed to load policies" onRetry={refreshPolicies} /> ), onSuccess: ({ value }) => { // Sort by owner rank (org outer, user inner), then position (lex // order on fractional-indexing keys), tiebreaking on id so // identical positions don't swap on refetch and // `generateKeyBetween` never sees duplicate neighbor keys (which // would throw). Optimistic placeholders carry `position: ""` so // they sort to the top of their owner group. const sorted = [...value].sort((a, b) => { const ownerOrder = ownerRank(a.owner) - ownerRank(b.owner); return ownerOrder === 0 ? comparePolicy(a.position, a.id, b.position, b.id) : ownerOrder; }); // Reorder math runs against committed rows only — placeholder rows // (empty `position`) aren't valid keys for `generateKeyBetween` and // aren't reorderable until the server confirms. const committedForOwner = (owner: Owner) => sorted.filter((p) => p.owner === owner && p.position !== ""); const committedIndex = (id: string, owner: Owner): number => committedForOwner(owner).findIndex((p) => p.id === id); const positionAbove = (id: string, owner: Owner): string => { const committed = committedForOwner(owner); const j = committedIndex(id, owner); if (j <= 0) return generateKeyBetween(null, committed[0]!.position); return j === 1 ? generateKeyBetween(null, committed[0]!.position) : generateKeyBetween(committed[j - 2]!.position, committed[j - 1]!.position); }; const positionBelow = (id: string, owner: Owner): string => { const committed = committedForOwner(owner); const j = committedIndex(id, owner); if (j === -1 || j >= committed.length - 1) return generateKeyBetween(committed[committed.length - 1]!.position, null); return j === committed.length - 2 ? generateKeyBetween(committed[committed.length - 1]!.position, null) : generateKeyBetween(committed[j + 1]!.position, committed[j + 2]!.position); }; return ( <CardStack> <CardStackHeader>Active policies</CardStackHeader> <CardStackContent> {sorted.length === 0 ? ( <CardStackEntry> <CardStackEntryContent> <CardStackEntryDescription> No policies yet. Tools fall back to their plugin's default approval behavior. </CardStackEntryDescription> </CardStackEntryContent> </CardStackEntry> ) : ( sorted.map((p) => { const committed = committedForOwner(p.owner); const j = committedIndex(p.id, p.owner); // Pending placeholder or only one committed row → no // reorder affordance. const reorderable = j !== -1 && committed.length > 1; return ( <PolicyRow key={p.id} policy={{ id: p.id, owner: p.owner, pattern: p.pattern, action: p.action, }} isFirst={!reorderable || j === 0} isLast={!reorderable || j === committed.length - 1} showOwnerLabel={ownerDisplay.showOwnerLabels} onRemove={() => setRemovingPolicy({ id: p.id, owner: p.owner, pattern: p.pattern }) } onChangeAction={(action) => handleUpdate({ id: p.id, owner: p.owner }, action) } onMoveUp={() => handleMove( { id: p.id, owner: p.owner }, positionAbove(p.id, p.owner), "up", ) } onMoveDown={() => handleMove( { id: p.id, owner: p.owner }, positionBelow(p.id, p.owner), "down", ) }