Guardrail Interlock Keys

Agent

Output guardrails built as a trapped-key interlock — every policy gate holds one key, the release bolt only retracts when all of them are seated, and a tripped gate is a visibly empty, barred keyway rather than a red badge.

Install
npx shadcn add https://design.helpmarq.com/r/guardrail-interlock-keys.json
Source
registry/core/guardrail-interlock-keys/component.tsx
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useReducer,
  useRef,
  useState,
} from "react";

// ---------------------------------------------------------------------------
// KeyInterlock — output guardrails drawn as a trapped-key interlock.
//
// Every policy gate physically holds one key. A gate that passed has its key
// SEATED in its keyway; a gate still evaluating has an EMPTY keyway with a
// breathing dashed outline; a gate that tripped has an empty keyway with a
// solid bar drawn across it, so it cannot accept a key at all. Every state is
// shape-coded — seated / empty-dashed / empty-barred — and there is no status
// hue anywhere in the component, so it survives monochrome and both themes.
//
// Released keys travel to the RELEASE LOCK on the right. The lock's bolt only
// retracts once every slot is filled; until then it stays shut across the
// output port and the output panel below renders dimmed behind a diagonal
// hatch. Overriding a tripped gate seats a HATCHED key instead of a solid one
// and writes a permanent audit line — an overridden interlock stays visually
// distinguishable from a passed one forever. That is the safety property.
// ---------------------------------------------------------------------------

export type InterlockGateState = "pending" | "released" | "tripped";

export type InterlockGate = {
  /** rule id, shown in Geist Mono and used in the audit line, e.g. "pii_scan" */
  id: string;
  label: string;
  state: InterlockGateState;
  /** one line shown in the mono strip while the gate is hovered or focused */
  detail?: string;
};

export interface KeyInterlockProps {
  gates: InterlockGate[];
  /** the model output the lock is holding */
  output: string;
  /** accessible name for the interlock group */
  label?: string;
  /** short caption above the row */
  title?: string;
  /** who is recorded on an override audit line */
  operator?: string;
  /** fired when a tripped gate is overridden — the owner flips that gate to
   * "released" (this component never mutates the gates it was handed) */
  onOverride?: (id: string) => void;
  className?: string;
}

const GATE_W = 40;
const GATE_H = 54;
const LOCK_W = 88;
/** keeps the slot run off the lock's own side walls, so the outermost slot is
 * never drawn half on top of the body outline */
const SLOT_INSET = 10;
const TRAVEL_MS = 420;
const TRAVEL_EASE = "cubic-bezier(0.22,1,0.36,1)";

type Flight = {
  id: string;
  x0: number;
  y0: number;
  x1: number;
  y1: number;
  hatched: boolean;
  moved: boolean;
};

function stamp() {
  return new Date().toLocaleTimeString("en-GB", {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  });
}

function stateWord(g: InterlockGate, overridden: boolean) {
  if (g.state === "released")
    return overridden ? "released by override" : "key seated";
  if (g.state === "tripped") return "tripped, keyway barred";
  return "awaiting key";
}

/** the key glyph: a 7px bit filling the keyway over a 3px stem */
function KeyGlyph({
  hatch,
  patternId,
}: {
  hatch: boolean;
  patternId: string;
}) {
  const fill = hatch ? `url(#${patternId})` : "currentColor";
  return (
    <>
      <rect
        x="0.5"
        y="0"
        width="7"
        height="8"
        fill={fill}
        stroke={hatch ? "currentColor" : "none"}
        strokeWidth={hatch ? 1 : 0}
      />
      <rect x="2.5" y="8" width="3" height="18" fill="currentColor" />
    </>
  );
}

export function KeyInterlock({
  gates,
  output,
  label = "Output guardrails",
  title = "Output interlock",
  operator = "operator",
  onOverride,
  className = "",
}: KeyInterlockProps) {
  const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
  const hatchId = `ns-ilk-hatch-${uid}`;

  const rowRef = useRef<HTMLDivElement>(null);
  const gateKeyRefs = useRef<Record<string, HTMLSpanElement | null>>({});
  const lockSlotRefs = useRef<Record<string, HTMLSpanElement | null>>({});

  // seeded from props so the very first paint (server and client alike) already
  // shows the keys that are in, instead of flashing an empty lock
  const arrivedRef = useRef<Set<string>>(
    new Set(gates.filter((g) => g.state === "released").map((g) => g.id))
  );
  const inFlightRef = useRef<Set<string>>(new Set());
  const overriddenRef = useRef<Set<string>>(new Set());
  const announcedRef = useRef<Set<string>>(new Set());
  const mountedRef = useRef(false);
  const timersRef = useRef<number[]>([]);
  const [, bump] = useReducer((n: number) => n + 1, 0);

  const [flights, setFlights] = useState<Flight[]>([]);
  const [audit, setAudit] = useState<string[]>([]);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [announce, setAnnounce] = useState("");
  const [reduced, setReduced] = useState(false);

  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const sync = () => setReduced(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);

  useEffect(
    () => () => {
      timersRef.current.forEach((t) => window.clearTimeout(t));
    },
    []
  );

  const later = useCallback((fn: () => void, ms: number) => {
    const t = window.setTimeout(fn, ms);
    timersRef.current.push(t);
  }, []);

  const startFlight = useCallback(
    (id: string) => {
      const row = rowRef.current;
      const from = gateKeyRefs.current[id];
      const to = lockSlotRefs.current[id];
      if (!row || !from || !to) {
        arrivedRef.current.add(id);
        bump();
        return;
      }
      const r = row.getBoundingClientRect();
      const a = from.getBoundingClientRect();
      const b = to.getBoundingClientRect();
      inFlightRef.current.add(id);
      const flight: Flight = {
        id,
        x0: a.left - r.left,
        y0: a.top - r.top,
        x1: b.left - r.left,
        y1: b.top - r.top,
        hatched: overriddenRef.current.has(id),
        moved: false,
      };
      setFlights((f) => [...f.filter((x) => x.id !== id), flight]);
      requestAnimationFrame(() =>
        requestAnimationFrame(() =>
          setFlights((f) =>
            f.map((x) => (x.id === id ? { ...x, moved: true } : x))
          )
        )
      );
      later(() => {
        inFlightRef.current.delete(id);
        arrivedRef.current.add(id);
        setFlights((f) => f.filter((x) => x.id !== id));
        bump();
      }, TRAVEL_MS);
    },
    [later]
  );

  // reconcile the lock against the gates it was handed: a gate that reaches
  // "released" sends its key across; a gate that leaves "released" (re-armed
  // upstream) takes its key back out of the lock.
  useEffect(() => {
    let changed = false;
    for (const g of gates) {
      const arrived = arrivedRef.current.has(g.id);
      const flying = inFlightRef.current.has(g.id);
      if (g.state === "released" && !arrived && !flying) {
        if (!mountedRef.current || reduced) {
          arrivedRef.current.add(g.id);
          changed = true;
        } else {
          startFlight(g.id);
        }
      }
      if (g.state !== "released" && arrived) {
        arrivedRef.current.delete(g.id);
        changed = true;
      }
    }
    mountedRef.current = true;
    if (changed) bump();
  }, [gates, reduced, startFlight]);

  // announce each trip exactly once
  useEffect(() => {
    const fresh = gates.filter(
      (g) => g.state === "tripped" && !announcedRef.current.has(g.id)
    );
    for (const g of gates) {
      if (g.state !== "tripped") announcedRef.current.delete(g.id);
    }
    if (fresh.length === 0) return;
    fresh.forEach((g) => announcedRef.current.add(g.id));
    setAnnounce(
      fresh.map((g) => `${g.id} tripped, output held`).join(". ") + "."
    );
  }, [gates]);

  useEffect(() => {
    if (!activeId) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "Escape") setActiveId(null);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [activeId]);

  const seated = gates.filter((g) => arrivedRef.current.has(g.id)).length;
  const released = seated === gates.length && gates.length > 0;
  const tripped = gates.filter((g) => g.state === "tripped");
  const active = gates.find((g) => g.id === activeId) ?? null;

  const slotXs = useMemo(() => {
    const n = Math.max(gates.length, 1);
    const step = (LOCK_W - SLOT_INSET * 2) / n;
    return gates.map((_, i) => SLOT_INSET + step * (i + 0.5));
  }, [gates]);

  function override(g: InterlockGate) {
    if (g.state !== "tripped") return;
    overriddenRef.current.add(g.id);
    // newest first, and the list is height-bounded rather than unbounded: an
    // audit trail that grows the component's own box forever makes the card
    // reframe on every entry. Nothing is ever dropped — it scrolls.
    setAudit((a) => [`override ${g.id} · ${operator} · ${stamp()}`, ...a]);
    setAnnounce(`${g.id} overridden by ${operator}`);
    onOverride?.(g.id);
  }

  return (
    <div
      data-interlock
      role="group"
      aria-label={label}
      className={"font-sans " + className}
    >
      <style>{`
.ns-ilk-breathe{animation:ns-ilk-breathe 3.2s ease-in-out infinite alternate}
@keyframes ns-ilk-breathe{from{opacity:.45}to{opacity:1}}
.ns-ilk-gate{transition:transform 180ms ease-out,opacity 180ms ease-out}
.ns-ilk-strip{transition:opacity 180ms ease-out,transform 180ms ease-out}
.ns-ilk-bolt{transition:transform ${TRAVEL_MS}ms cubic-bezier(0.34,1.8,0.64,1)}
.ns-ilk-fly{transition:transform ${TRAVEL_MS}ms ${TRAVEL_EASE}}
.ns-ilk-veil{transition:opacity 320ms ease-out}
@media (prefers-reduced-motion: reduce){
  .ns-ilk-breathe{animation:none;opacity:.75}
  .ns-ilk-gate,.ns-ilk-strip,.ns-ilk-bolt,.ns-ilk-fly,.ns-ilk-veil{transition:none}
}
`}</style>

      <div className="mb-4 flex items-end justify-between gap-4">
        <p className="font-mono text-[11px] uppercase tracking-[0.18em] text-muted">
          {title}
        </p>
        <p
          data-interlock-readout
          className="shrink-0 font-mono text-[11px] text-foreground"
        >
          {seated}/{gates.length} keys
          <span className="px-1.5 text-muted">·</span>
          {released ? "output released" : "output held"}
        </p>
      </div>

      {/* ---- the interlock row ------------------------------------------ */}
      <div
        ref={rowRef}
        data-interlock-row
        className="relative flex items-start"
        onPointerLeave={() => setActiveId(null)}
        onBlur={() => setActiveId(null)}
      >
        <div className="flex items-start gap-3">
          {gates.map((g, i) => {
            const dim = activeId !== null && activeId !== g.id;
            const lift = activeId === g.id;
            const overridden = overriddenRef.current.has(g.id);
            return (
              <button
                key={g.id}
                type="button"
                data-interlock-gate={g.id}
                data-state={g.state}
                aria-label={`${g.label}: ${stateWord(g, overridden)}${
                  g.detail ? `. ${g.detail}` : ""
                }`}
                onPointerEnter={() => setActiveId(g.id)}
                onFocus={() => setActiveId(g.id)}
                // a tap/click pins this gate's detail. Deliberately NOT a
                // toggle: pointerenter has already set activeId, so toggling
                // would blank the strip on the very click that asked for it.
                onClick={() => setActiveId(g.id)}
                className={
                  "ns-ilk-gate group flex w-16 flex-col items-center rounded-sm text-foreground focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent " +
                  (dim ? "opacity-30 " : "opacity-100 ") +
                  (lift ? "-translate-y-0.5" : "translate-y-0")
                }
              >
                <span className="relative block">
                  {/* keyway centre marker — the flight origin */}
                  <span
                    ref={(el) => {
                      gateKeyRefs.current[g.id] = el;
                    }}
                    aria-hidden
                    className="pointer-events-none absolute left-[16.5px] top-[3px] block h-0 w-0"
                  />
                  <svg
                    width={GATE_W}
                    height={GATE_H}
                    viewBox={`0 0 ${GATE_W} ${GATE_H}`}
                    fill="none"
                    aria-hidden
                    className="block text-foreground"
                  >
                    <defs>
                      <pattern
                        id={`${hatchId}-${i}`}
                        width="4"
                        height="4"
                        patternUnits="userSpaceOnUse"
                        patternTransform="rotate(45)"
                      >
                        <rect
                          width="2"
                          height="4"
                          fill="currentColor"
                          opacity="0.85"
                        />
                      </pattern>
                    </defs>
                    {/* slot body with the keyway notch cut into its top edge */}
                    <path
                      d="M0.5 53.5 V0.5 H16.5 V14 H23.5 V0.5 H39.5 V53.5 Z"
                      className="stroke-border"
                      strokeWidth="1"
                    />
                    {g.state === "released" ? (
                      <g transform="translate(16,3.5)">
                        <KeyGlyph
                          hatch={overridden}
                          patternId={`${hatchId}-${i}`}
                        />
                      </g>
                    ) : null}
                    {g.state === "pending" ? (
                      <rect
                        className="ns-ilk-breathe stroke-foreground"
                        style={{ animationDelay: `${((i * 260) % 1040) - 520}ms` }}
                        x="16.75"
                        y="2.75"
                        width="6.5"
                        height="10.5"
                        strokeWidth="1.5"
                        strokeDasharray="2 2"
                        fill="none"
                      />
                    ) : null}
                    {g.state === "tripped" ? (
                      <line
                        x1="8"
                        y1="18"
                        x2="32"
                        y2="4.1"
                        className="stroke-foreground"
                        strokeWidth="2"
                        strokeLinecap="round"
                      />
                    ) : null}
                  </svg>
                </span>
                <span
                  className={
                    "mt-2 block w-full break-all text-center font-mono text-[10px] leading-[1.15] " +
                    (g.state === "tripped"
                      ? "font-medium text-foreground"
                      : "text-muted")
                  }
                  title={g.id}
                >
                  {g.id}
                </span>
              </button>
            );
          })}
        </div>

        {/* rail from the gates to the lock */}
        <div className="relative mx-3 h-[54px] w-8 shrink-0" aria-hidden>
          <div className="absolute inset-x-0 top-[9px] h-px bg-border" />
        </div>

        {/* ---- the release lock ----------------------------------------- */}
        <div className="flex shrink-0 flex-col items-center">
          <span className="relative block">
            {gates.map((g, i) => (
              <span
                key={g.id}
                ref={(el) => {
                  lockSlotRefs.current[g.id] = el;
                }}
                aria-hidden
                className="pointer-events-none absolute top-[8px] block h-0 w-0"
                style={{ left: `${slotXs[i] - 3.5}px` }}
              />
            ))}
            <svg
              width={LOCK_W}
              height={GATE_H}
              viewBox={`0 0 ${LOCK_W} ${GATE_H}`}
              fill="none"
              aria-hidden
              className="block text-foreground"
            >
              <defs>
                <pattern
                  id={`${hatchId}-lock`}
                  width="4"
                  height="4"
                  patternUnits="userSpaceOnUse"
                  patternTransform="rotate(45)"
                >
                  <rect width="2" height="4" fill="currentColor" opacity="0.85" />
                </pattern>
              </defs>
              {/* lock body, open at the bottom where the output port is */}
              <path
                d="M32.5 53.5 H0.5 V0.5 H87.5 V53.5 H55.5"
                className="stroke-border"
                strokeWidth="1"
              />
              {gates.map((g, i) => {
                const filled = arrivedRef.current.has(g.id);
                const overridden = overriddenRef.current.has(g.id);
                return (
                  <rect
                    key={g.id}
                    data-lock-slot={g.id}
                    data-filled={filled ? "true" : "false"}
                    x={slotXs[i] - 3.5}
                    y="8"
                    width="7"
                    height="12"
                    fill={
                      filled
                        ? overridden
                          ? `url(#${hatchId}-lock)`
                          : "currentColor"
                        : "none"
                    }
                    className={filled ? "stroke-foreground" : "stroke-border"}
                    strokeWidth="1"
                  />
                );
              })}
              {/* the bolt: shut across the output port until every key is in */}
              <rect
                data-interlock-bolt
                data-retracted={released ? "true" : "false"}
                className="ns-ilk-bolt fill-current"
                x="33"
                y="44"
                width="22"
                height="6"
                rx="1"
                style={{
                  transform: released ? "translateX(20px)" : "translateX(0)",
                }}
              />
            </svg>
          </span>
          <span className="mt-2 block font-mono text-[9px] leading-none text-muted">
            release lock
          </span>
        </div>

        {/* ---- key travel overlay --------------------------------------- */}
        <div className="pointer-events-none absolute inset-0" aria-hidden>
          {flights.length > 0 ? (
            <svg className="absolute inset-0 h-full w-full" fill="none">
              {flights.map((f) => (
                <line
                  key={f.id}
                  x1={f.x0 + 4}
                  y1={f.y0 + 4}
                  x2={f.x1 + 4}
                  y2={f.y1 + 4}
                  className="stroke-border"
                  strokeWidth="1"
                />
              ))}
            </svg>
          ) : null}
          {flights.map((f) => (
            <svg
              key={f.id}
              width="8"
              height="26"
              viewBox="0 0 8 26"
              fill="none"
              className="ns-ilk-fly absolute left-0 top-0 text-foreground"
              style={{
                transform: f.moved
                  ? `translate(${f.x1}px, ${f.y1}px)`
                  : `translate(${f.x0}px, ${f.y0}px)`,
              }}
            >
              <defs>
                <pattern
                  id={`${hatchId}-fly-${f.id}`}
                  width="4"
                  height="4"
                  patternUnits="userSpaceOnUse"
                  patternTransform="rotate(45)"
                >
                  <rect width="2" height="4" fill="currentColor" opacity="0.85" />
                </pattern>
              </defs>
              <KeyGlyph
                hatch={f.hatched}
                patternId={`${hatchId}-fly-${f.id}`}
              />
            </svg>
          ))}
        </div>
      </div>

      {/* ---- detail strip ------------------------------------------------ */}
      <div
        id={`${uid}-strip`}
        className="ns-ilk-strip mt-4 h-[16px] font-mono text-[11px] leading-[16px] text-muted"
        style={{
          opacity: active ? 1 : 0,
          transform: active ? "translateY(0)" : "translateY(-2px)",
        }}
      >
        {active ? (
          <>
            <span className="text-foreground">{active.id}</span>
            <span className="px-1.5">·</span>
            {active.detail ?? stateWord(active, overriddenRef.current.has(active.id))}
          </>
        ) : null}
      </div>

      {/* ---- override controls ------------------------------------------- */}
      {tripped.length > 0 ? (
        <div className="mt-3 flex flex-wrap gap-2">
          {tripped.map((g) => (
            <button
              key={g.id}
              type="button"
              data-override={g.id}
              onClick={() => override(g)}
              className="rounded-sm border border-border px-2.5 py-1 font-mono text-[11px] uppercase tracking-wide text-foreground transition-colors hover:bg-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
            >
              override {g.id}
            </button>
          ))}
        </div>
      ) : null}

      {/* ---- the held output --------------------------------------------- */}
      <div className="relative mt-5">
        <div
          className="ns-ilk-veil rounded-md border border-border bg-surface p-4"
          style={{ opacity: released ? 1 : 0.35 }}
        >
          <p className="mb-2 font-mono text-[10px] uppercase tracking-[0.18em] text-muted">
            model output
          </p>
          <p className="text-sm leading-relaxed text-foreground">{output}</p>
        </div>
        <div
          aria-hidden
          className="ns-ilk-veil pointer-events-none absolute inset-0 rounded-md"
          style={{
            opacity: released ? 0 : 1,
            backgroundImage:
              "repeating-linear-gradient(45deg, color-mix(in srgb, var(--foreground) 8%, transparent) 0 1px, transparent 1px 4px)",
          }}
        />
      </div>

      {/* ---- permanent audit trail --------------------------------------- */}
      {audit.length > 0 ? (
        <div className="mt-4 border-t border-border pt-3">
          <p className="mb-1.5 font-mono text-[10px] uppercase tracking-[0.18em] text-muted">
            audit
          </p>
          {/* exactly three 17px lines + two 4px gaps — a partial clipped line
              would read as a rendering bug rather than a scroll affordance */}
          <ul className="max-h-[59px] space-y-1 overflow-y-auto">
            {audit.map((line, i) => (
              <li
                key={i}
                data-audit-line
                className="font-mono text-[11px] text-foreground"
              >
                {line}
              </li>
            ))}
          </ul>
        </div>
      ) : null}

      <p role="status" aria-live="polite" className="sr-only">
        {announce}
      </p>
    </div>
  );
}
Use when

several independent policy checks must ALL pass before an agent's output is allowed through, and the reason it is still held — which specific gate is empty — has to be readable from a still frame; the bolt/key mechanism also makes an operator override permanently distinguishable from a genuine pass. Pick approval-inline-diff instead when one human approves or edits a single proposed tool call, meter-threshold-trip when one metric crossing one threshold is the whole story, and refusal-negotiation when the point is what the model will do instead of the refused request.

Build spec

`<KeyInterlock gates={InterlockGate[]} output label? title? operator? onOverride? className? />` renders a set of output guardrails as a mechanical trapped-key interlock instead of a row of coloured status badges. Each gate is `{ id, label, state: 'pending' | 'released' | 'tripped', detail? }` and draws as a 40x54 SVG slot: a 1px `stroke-border` outline path with a 7px-wide keyway notch physically cut into its top edge (`M0.5 53.5 V0.5 H16.5 V14 H23.5 V0.5 H39.5 V53.5 Z`), so the shape itself says 'this slot takes a key'. STATE IS ENTIRELY SHAPE-CODED, never hue-coded — there is no status colour anywhere in the component, so it reads identically in monochrome and in both themes. RELEASED draws the key SEATED: a 7x8 bit filling the keyway over a 3px stem running 18px down into the slot body, solid `currentColor` (`--foreground`). PENDING draws the keyway EMPTY with a 1.5px `stroke-dasharray='2 2'` outline breathing opacity 0.45 -> 1 on a 3.2s ease-in-out infinite alternate keyframe, each gate offset by a deterministic index-derived `animationDelay` of `((i * 260) % 1040) - 520` ms (a -520..+260ms spread in 260ms steps) so the row does not pulse in lockstep — this is the only ambient motion in the piece. TRIPPED draws the keyway empty AND physically obstructed: a 2px `--foreground` line from (8,18) to (32,4.1), i.e. a bar raked at 30 degrees across the notch, so the slot visibly cannot accept a key; the rule id sits beneath every gate in Geist Mono and goes to full `--foreground` medium weight on the tripped one. To the right, past a 1px `--border` rail, sits the RELEASE LOCK: an 88x54 body whose outline path deliberately leaves a 23px gap in its bottom edge (`M32.5 53.5 H0.5 V0.5 H87.5 V53.5 H55.5`) — that gap is the output port. The lock carries one 7x12 slot per gate across its top, evenly spaced at `SLOT_INSET + (LOCK_W - 2*SLOT_INSET) / n * (i + 0.5)` with `SLOT_INSET = 10` so the run keeps clear of the body's own side walls and the slots read as separate keyholes rather than one filled bar, outlined in `--border` when empty and filled solid `--foreground` when its key has arrived. KEY TRAVEL: a gate that reaches 'released' after mount launches a key glyph from its own keyway to its slot in the lock — measured live against the row's bounding box via two zero-size marker spans (one at the keyway, one at the lock slot), rendered in a `pointer-events-none` overlay, and translated over 420ms on `cubic-bezier(0.22,1,0.36,1)` with a 1px `--border` guide line drawn along the same path for the duration; on arrival the flight unmounts and the lock slot fills. Gates that are already 'released' at mount seat instantly with no flight, and a gate the owner re-arms out of 'released' takes its key back out of the lock. THE BOLT is a 22x6 bar sitting across the output port; it is rendered retracted (`translateX(20px)`, on a 420ms `cubic-bezier(0.34,1.8,0.64,1)` that genuinely overshoots ~3px before settling) ONLY when `seatedCount === gates.length`, and extended across the port otherwise. While the bolt is shut the output panel below renders at 0.35 opacity behind a 4px-pitch 45-degree diagonal hatch built as a `repeating-linear-gradient` of `color-mix(in srgb, var(--foreground) 8%, transparent)`; both the dim and the hatch lift over 320ms once the bolt retracts. A Geist Mono readout above the row states the count and the consequence in one line: '3/5 keys · output held'. OVERRIDE is a real `<button>` per tripped gate, labelled 'override <ruleId>' in mono with a visible `--accent` focus ring; pressing it records the gate in an internal overridden set and seats a HATCHED key — an SVG `pattern` of 2px diagonal stripes at 45 degrees instead of a solid bit, in both the gate keyway and the lock slot — and pushes a permanent audit line ('override pii_scan · n.sapalidis · 14:02:31') that no prop, press or re-arm ever clears. The trail is newest-first inside a 59px `overflow-y-auto` list (exactly three 17px lines plus their two 4px gaps, so no line is ever half-clipped) — every entry is kept, but the component's own box stops growing after the third one, so a repeatedly-overridden interlock does not slowly reframe the layout around it. An overridden interlock therefore stays visually distinguishable from a genuinely passed one for the life of the component, which is the actual safety property being modelled. The component is controlled: it never mutates the `gates` array it is handed, it calls `onOverride(id)` and lets the owner decide what the gate becomes. INTERACTION: hovering (or keyboard-focusing) a gate lifts it 2px, holds it at full alpha, drops every other gate to 0.3 opacity and reveals that gate's `detail` in a fixed-height Geist Mono strip under the row, all on 180ms ease-out; `pointerleave` on the group relaxes all of it on the same timing, and Escape dismisses the strip. Each gate is itself a real `<button>` carrying an `aria-label` that spells out label, state and detail, so a keyboard user gets in one string exactly what a hover reveals in the strip; `focusout` on the row clears the strip and the dimming, so tabbing past the last gate leaves nothing stale behind. ACCESSIBILITY: the whole component is a `role=group` with an accessible name; a visually-hidden `role=status`/`aria-live=polite` region announces each trip exactly once ('pii_scan tripped, output held') and each override as it happens, rather than chattering on every render; every focusable control carries a 2px `--accent` focus ring (offset 4px on the gates so it clears the lifted slot, 2px on the override buttons); the SVGs are `aria-hidden` decorations sitting behind text that already carries the semantics. REDUCED MOTION (`matchMedia` with a live change listener, plus a CSS `prefers-reduced-motion: reduce` block) makes key travel and bolt retraction instant, drops the pending breathe to a static 0.75 opacity dashed outline, and removes the hover and hatch transitions — every state stays fully legible, just static. Colours are `--background --foreground --muted --border --surface --accent` only, via Tailwind token classes and `currentColor`; `--accent` appears solely as the focus ring. DOM + SVG + CSS, no canvas, zero dependencies.

Tags
agentllmguardrailpolicysafetyinterlockapprovalmonostatusaccessibility