import { useState } from "react"; import { 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 { 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"; // 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("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 (
{ e.preventDefault(); if (!valid) return; props.onSubmit({ owner: props.owner, pattern, action }); setPattern(""); setAction("require_approval"); }} >
setPattern(e.target.value)} className="font-mono text-sm" />

Exact tool id, trailing wildcard, or * for every tool. Examples: *,{" "} vercel.*,{" "} vercel.dns.*,{" "} vercel.dns.create.

{ownerChoices.length > 1 ? (
) : null}
); } // --------------------------------------------------------------------------- // 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 ( {props.policy.pattern} {props.showOwnerLabel ? ( {ownerLabel(props.policy.owner)} ) : null} Move up Move down Remove ); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export function PoliciesPage() { const policies = useAtomValue(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 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("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 }) => { 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 (

Policies

Override default approval behavior for tools. The most restrictive matched action wins. Blocked tools are hidden from agent search and fail at invoke.

{AsyncResult.match(policies, { onInitial: () => (

Loading policies…

), onFailure: () => (

Failed to load policies

), 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 ( Active policies {sorted.length === 0 ? ( No policies yet. Tools fall back to their plugin's default approval behavior. ) : ( 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 ( handleRemove({ id: p.id, owner: p.owner })} 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", ) } /> ); }) )} ); }, })}
); } // Exported for tests / direct consumers that don't want the matcher // duplicated in two places. Cloud's UI uses these for live preview. export { matchesPattern, isValidPattern };