> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coding Agent Governance

> Secure your code generation factory.

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

export const SetupWizard = ({configUrl}) => {
  const [config, setConfig] = useState(null);
  const [loadError, setLoadError] = useState('');
  const [isDark, setIsDark] = useState(false);
  const [currentStep, setCurrentStep] = useState(1);
  const [selections, setSelections] = useState({});
  const [options, setOptions] = useState({});
  const [toast, setToast] = useState('');
  const [initialized, setInitialized] = useState(false);
  const loadYamlParser = () => new Promise((resolve, reject) => {
    if (globalThis.jsyaml) {
      resolve(globalThis.jsyaml);
      return;
    }
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js';
    script.onload = () => resolve(globalThis.jsyaml);
    script.onerror = () => reject(new Error('Failed to load YAML parser'));
    document.head.appendChild(script);
  });
  useEffect(() => {
    if (!configUrl) return;
    let cancelled = false;
    Promise.all([fetch(configUrl).then(r => {
      if (!r.ok) {
        throw new Error(r.status + ' ' + r.statusText);
      }
      return r.text();
    }), loadYamlParser()]).then(([yamlText, jsyaml]) => {
      if (cancelled) return;
      const data = jsyaml.load(yamlText);
      if (data?.steps) setConfig(data); else setLoadError('Invalid wizard configuration.');
    }).catch(err => {
      if (!cancelled) setLoadError('Unable to load wizard: ' + err.message);
    });
    return () => {
      cancelled = true;
    };
  }, [configUrl]);
  useEffect(() => {
    if (!config || initialized) return;
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      const raw = localStorage.getItem(sk);
      if (raw) {
        const saved = JSON.parse(raw);
        if (saved.step >= 1 && saved.step <= config.steps.length) setCurrentStep(saved.step);
        if (saved.selections) setSelections(saved.selections);
        if (saved.options) setOptions(saved.options);
      }
    } catch {}
    setInitialized(true);
  }, [config, initialized]);
  useEffect(() => {
    if (!config || !initialized) return;
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      localStorage.setItem(sk, JSON.stringify({
        step: currentStep,
        selections,
        options
      }));
    } catch {}
  }, [config, initialized, currentStep, selections, options]);
  useEffect(() => {
    const check = () => {
      const r = document.documentElement;
      setIsDark(r.dataset.theme === 'dark' || r.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => obs.disconnect();
  }, []);
  useEffect(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(''), 2500);
    return () => clearTimeout(t);
  }, [toast]);
  if (loadError) return <div style={{
    padding: '1rem',
    color: '#d32f2f',
    fontSize: '0.9rem'
  }}>{loadError}</div>;
  if (!config) return <div style={{
    padding: '1rem',
    opacity: 0.6,
    fontSize: '0.9rem'
  }}>Loading wizard...</div>;
  const steps = config.steps;
  const totalSteps = steps.length;
  const getSelectedItem = (dataKey, sels) => {
    const id = sels[dataKey];
    if (!id) return null;
    const items = config[dataKey];
    return items ? items.find(it => it.id === id) || null : null;
  };
  const getDocLabel = (docs, urlKey, fallback) => {
    if (!docs) return fallback || '';
    return docs[urlKey.replace(/_url$/, '_label')] || fallback || docs[urlKey] || '';
  };
  const getDocEntries = docs => {
    if (!docs) return [];
    return Object.keys(docs).filter(k => k.endsWith('_url')).map(k => ({
      key: k.replace(/_url$/, ''),
      url: docs[k],
      label: getDocLabel(docs, k, k.replace(/_url$/, ''))
    }));
  };
  const getProviderCaps = () => {
    for (const step of steps) {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item?.capabilities) return item.capabilities;
      }
    }
    return {};
  };
  const canProceed = stepNum => {
    const step = steps[stepNum - 1];
    if (!step) return false;
    if (step.type === 'choice_grid') return Boolean(selections[step.data_key]);
    if (step.type === 'plan_output') return false;
    return true;
  };
  const handleNext = () => {
    if (currentStep < totalSteps && canProceed(currentStep)) setCurrentStep(currentStep + 1);
  };
  const handleBack = () => {
    if (currentStep > 1) setCurrentStep(currentStep - 1);
  };
  const handleStepClick = n => {
    if (n <= currentStep) setCurrentStep(n);
  };
  const handleSelect = (dataKey, id) => {
    setSelections(prev => ({
      ...prev,
      [dataKey]: id
    }));
  };
  const handleOptionToggle = (id, checked) => {
    setOptions(prev => ({
      ...prev,
      [id]: checked
    }));
  };
  const handleReset = () => {
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      localStorage.removeItem(sk);
    } catch {}
    setCurrentStep(1);
    setSelections({});
    setOptions({});
    setToast('Reset complete.');
  };
  const buildChecklistLines = lines => {
    let sn = 1;
    steps.forEach(step => {
      if (step.type !== 'choice_grid') return;
      const item = getSelectedItem(step.data_key, selections);
      if (!item) return;
      if (item.pre_setup_title && item.docs) {
        lines.push(sn + ') ' + item.pre_setup_title);
        getDocEntries(item.docs).forEach(d => lines.push('   - ' + d.label + ': ' + d.url));
        if (item.warning) lines.push('   Important: ' + item.warning);
        sn++;
      } else if (item.docs) {
        if (item.docs.overview_url) {
          lines.push(sn + ') ' + getDocLabel(item.docs, 'overview_url', 'Install'), '   - ' + item.docs.overview_url);
          sn++;
        }
        if (item.docs.manage_url) {
          lines.push(sn + ') ' + getDocLabel(item.docs, 'manage_url', 'Configure'), '   - ' + item.docs.manage_url);
          sn++;
        }
      }
    });
  };
  const buildPlanText = () => {
    const lines = ['Setup plan: ' + (config.wizard.title || 'Setup Wizard'), '', 'Selections'];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item) lines.push('- ' + (step.label || step.data_key) + ': ' + (item.label || item.title || item.id));
      }
    });
    lines.push('', 'Checklist');
    buildChecklistLines(lines);
    const caps = getProviderCaps();
    const isSupported = item => {
      const capKey = item.capability_key || item.id;
      return capKey === 'pr_comments' ? Boolean(caps.pr_comments || caps.pr_checks) : Boolean(caps[capKey]);
    };
    steps.forEach(step => {
      if (step.type === 'checkbox_group') {
        const items = (config[step.data_key] || []).filter(isSupported);
        if (items.length === 0) return;
        lines.push('', step.label || 'Options');
        items.forEach(item => {
          const on = (item.id in options) ? Boolean(options[item.id]) : Boolean(item.default);
          let l = '- ' + (item.label || item.id) + ': ' + (on ? 'on' : 'off');
          if (on && item.url) l += ' (' + item.url + ')';
          lines.push(l);
        });
      }
    });
    return lines.join('\n');
  };
  const handleCopyPlan = () => {
    navigator.clipboard.writeText(buildPlanText()).then(() => setToast('Plan copied.')).catch(() => setToast('Copy failed.'));
  };
  const bgColor = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const bd = 'rgba(38,208,124,';
  const green = '#26D07C';
  const btnBase = {
    padding: '0.4rem 0.75rem',
    border: '1px solid',
    borderRadius: '10px',
    cursor: 'pointer',
    fontSize: '0.85rem',
    fontWeight: 600,
    display: 'inline-flex',
    alignItems: 'center',
    gap: '0.35rem',
    transition: 'all 0.15s ease'
  };
  const btnPrimary = {
    ...btnBase,
    background: green,
    borderColor: green,
    color: isDark ? '#000' : '#0A2E1D'
  };
  const btnSecondary = {
    ...btnBase,
    background: 'transparent',
    borderColor: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.15)',
    color: textColor,
    opacity: 0.85
  };
  const codeStyle = {
    fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
    fontSize: '0.85em',
    background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
    border: '1px solid ' + bd + '0.15)',
    borderRadius: '6px',
    padding: '0.1rem 0.35rem'
  };
  const h5s = {
    margin: '0.75rem 0 0.35rem',
    fontSize: '0.9rem',
    fontWeight: 700,
    opacity: 0.95
  };
  const renderChoiceGrid = step => {
    const items = config[step.data_key] || [];
    const cols = step.columns || 3;
    return <div className="sw-grid" style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(' + cols + ', 1fr)',
      gap: '0.75rem'
    }}>
        {items.map(item => {
      const sel = selections[step.data_key] === item.id;
      return <button key={item.id} type="button" onClick={() => handleSelect(step.data_key, item.id)} aria-pressed={sel} style={{
        background: sel ? bgColor : bgLight,
        border: '1px solid ' + bd + (sel ? '0.6)' : '0.15)'),
        borderRadius: '12px',
        padding: '0.75rem',
        cursor: 'pointer',
        transition: 'all 0.15s ease',
        textAlign: 'left',
        font: 'inherit',
        color: 'inherit'
      }}>
              <div style={{
        fontWeight: 700,
        marginBottom: '0.25rem',
        fontSize: '0.9rem'
      }}>{item.label || item.title || item.id}</div>
              <div style={{
        fontSize: '0.8rem',
        opacity: 0.85,
        lineHeight: 1.35
      }}>{item.description || item.summary || ''}</div>
            </button>;
    })}
      </div>;
  };
  const renderCheckboxGroup = step => {
    const caps = getProviderCaps();
    const supported = (config[step.data_key] || []).filter(item => {
      const capKey = item.capability_key || item.id;
      return capKey === 'pr_comments' ? Boolean(caps.pr_comments || caps.pr_checks) : Boolean(caps[capKey]);
    });
    return <div>
        <div className="sw-opts" style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, 1fr)',
      gap: '0.75rem'
    }}>
          {supported.map(item => {
      const checked = (item.id in options) ? options[item.id] : Boolean(item.default);
      return <div key={item.id} style={{
        background: bgLight,
        border: '1px solid ' + bd + '0.12)',
        borderRadius: '12px',
        padding: '0.75rem'
      }}>
                <label style={{
        fontWeight: 600,
        display: 'flex',
        gap: '0.5rem',
        alignItems: 'center',
        cursor: 'pointer'
      }}>
                  <input type="checkbox" checked={checked} onChange={e => handleOptionToggle(item.id, e.target.checked)} style={{
        accentColor: green
      }} />
                  {item.label || item.id}
                </label>
                <div style={{
        fontSize: '0.78rem',
        opacity: 0.8,
        marginTop: '0.25rem',
        lineHeight: 1.35
      }}>{item.help || ''}</div>
              </div>;
    })}
        </div>
      </div>;
  };
  const renderPlanOutput = () => {
    let hasSelections = false;
    steps.forEach(step => {
      if (step.type === 'choice_grid' && getSelectedItem(step.data_key, selections)) hasSelections = true;
    });
    if (!hasSelections) return <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.12)',
      borderRadius: '12px',
      padding: '0.75rem',
      fontSize: '0.9rem'
    }}>Make selections to generate a plan.</div>;
    const selItems = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item) selItems.push({
          label: step.label || step.data_key,
          value: item.label || item.title || item.id
        });
      }
    });
    const checkItems = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (!item) return;
        if (item.pre_setup_title && item.docs) checkItems.push({
          title: item.pre_setup_title,
          docs: getDocEntries(item.docs),
          warning: item.warning
        }); else if (item.docs) {
          if (item.docs.overview_url) checkItems.push({
            title: getDocLabel(item.docs, 'overview_url', 'Installation guide'),
            url: item.docs.overview_url
          });
          if (item.docs.manage_url) checkItems.push({
            title: getDocLabel(item.docs, 'manage_url', 'Configuration guide'),
            url: item.docs.manage_url
          });
        }
      }
    });
    const optSections = [];
    steps.forEach(step => {
      if (step.type === 'checkbox_group') {
        const items = config[step.data_key] || [];
        if (items.length > 0) optSections.push({
          label: step.label || step.title || 'Options',
          items
        });
      }
    });
    const extraDocs = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item?.docs && !item.pre_setup_title) Object.keys(item.docs).forEach(k => {
          if (k.endsWith('_url') && k !== 'overview_url' && k !== 'manage_url') extraDocs.push({
            url: item.docs[k],
            label: getDocLabel(item.docs, k, k.replace(/_url$/, ''))
          });
        });
      }
    });
    return <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.12)',
      borderRadius: '12px',
      padding: '0.75rem',
      fontSize: '0.9rem',
      lineHeight: 1.45
    }}>
        <h4 style={{
      margin: '0 0 0.4rem',
      fontSize: '0.95rem',
      fontWeight: 700
    }}>Setup plan: {config.wizard.title || 'Setup Wizard'}</h4>
        <h5 style={h5s}>Selections</h5>
        <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{selItems.map(s => <li key={s.label} style={{
      margin: '0.2rem 0'
    }}>{s.label}: <code style={codeStyle}>{s.value}</code></li>)}</ul>
        <h5 style={h5s}>Checklist</h5>
        <ol style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>
          {checkItems.map(c => <li key={c.url || c.title} style={{
      margin: '0.2rem 0'
    }}>
              {c.url ? <a href={c.url} style={{
      textDecoration: 'underline'
    }}>{c.title}</a> : c.title}
              {c.docs && <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{c.docs.map(d => <li key={d.key}><a href={d.url} style={{
      textDecoration: 'underline'
    }}>{d.label}</a></li>)}</ul>}
              {c.warning && <div style={{
      marginTop: '0.35rem',
      opacity: 0.9
    }}>Important: {c.warning}</div>}
            </li>)}
        </ol>
        {optSections.map(sec => <div key={sec.label}>
            <h5 style={h5s}>{sec.label}</h5>
            <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>
              {sec.items.filter(item => {
      const capKey = item.capability_key || item.id;
      const planCaps = getProviderCaps();
      return capKey === 'pr_comments' ? Boolean(planCaps.pr_comments || planCaps.pr_checks) : Boolean(planCaps[capKey]);
    }).map(item => {
      const on = (item.id in options) ? Boolean(options[item.id]) : Boolean(item.default);
      return <li key={item.id} style={{
        margin: '0.2rem 0'
      }}>{on && item.url ? <><a href={item.url} style={{
        textDecoration: 'underline'
      }}>{item.label || item.id}</a>: </> : (item.label || item.id) + ': '}<code style={codeStyle}>{on ? 'on' : 'off'}</code></li>;
    })}
            </ul>
          </div>)}
        {extraDocs.length > 0 && <div><h5 style={h5s}>Additional resources</h5><ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{extraDocs.map(d => <li key={d.url} style={{
      margin: '0.2rem 0'
    }}><a href={d.url} style={{
      textDecoration: 'underline'
    }}>{d.label}</a></li>)}</ul></div>}
      </div>;
  };
  return <div className="not-prose sw" style={{
    margin: '1rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div style={{
    background: bgColor,
    border: '1px solid ' + bd + '0.2)',
    borderRadius: '16px',
    padding: '1.25rem',
    color: textColor
  }}>
        <div style={{
    display: 'flex',
    gap: '0.75rem',
    alignItems: 'center',
    marginBottom: '0.75rem'
  }}>
          <div style={{
    width: '38px',
    height: '38px',
    borderRadius: '10px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    background: bgLight,
    border: '1px solid ' + bd + '0.2)',
    color: green,
    flexShrink: 0,
    fontSize: '1.1rem'
  }}>🗺</div>
          <div>
            <h3 style={{
    margin: '0 0 0.15rem',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: textColor
  }}>{config.wizard.title}</h3>
            {config.wizard.subtitle && <p style={{
    margin: 0,
    fontSize: '0.85rem',
    opacity: 0.8
  }}>{config.wizard.subtitle}</p>}
          </div>
        </div>
        <div className="sw-stepper" style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(' + totalSteps + ', 1fr)',
    gap: '0.5rem',
    margin: '0.75rem 0 1rem'
  }}>
          {steps.map((step, i) => {
    const n = i + 1;
    const active = n === currentStep;
    return <button key={step.id} type="button" onClick={() => handleStepClick(n)} disabled={n > currentStep} style={{
      border: '1px solid ' + bd + (active ? '0.4)' : '0.2)'),
      borderRadius: '10px',
      padding: '0.5rem 0.6rem',
      background: active ? bgColor : bgLight,
      color: textColor,
      opacity: active ? 1 : 0.8,
      cursor: n <= currentStep ? 'pointer' : 'default',
      fontWeight: 600,
      fontSize: '0.8rem',
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.5rem',
      justifyContent: 'center'
    }}>
                <span style={{
      width: '22px',
      height: '22px',
      borderRadius: '999px',
      display: 'inline-flex',
      alignItems: 'center',
      justifyContent: 'center',
      background: 'rgba(38,208,124,0.15)',
      color: textColor,
      fontSize: '0.75rem'
    }}>{n}</span>
                {step.label}
              </button>;
  })}
        </div>
        {steps.map((step, i) => {
    const n = i + 1;
    if (n !== currentStep) return null;
    return <div key={step.id}>
              <div style={{
      fontWeight: 600,
      margin: '0 0 0.75rem',
      fontSize: '0.95rem'
    }}>{step.title}</div>
              {step.type === 'choice_grid' && renderChoiceGrid(step)}
              {step.type === 'checkbox_group' && renderCheckboxGroup(step)}
              {step.type === 'plan_output' && <div>
                  <div style={{
      display: 'flex',
      gap: '0.5rem',
      justifyContent: 'flex-end',
      flexWrap: 'wrap',
      marginBottom: '0.5rem'
    }}>
                    <button type="button" onClick={handleReset} style={btnSecondary}>↺ Reset</button>
                    <button type="button" onClick={handleCopyPlan} style={btnSecondary}>📋 Copy plan</button>
                  </div>
                  {toast && <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.2)',
      borderRadius: '10px',
      padding: '0.5rem 0.75rem',
      margin: '0.5rem 0',
      fontSize: '0.85rem'
    }}>{toast}</div>}
                  {renderPlanOutput()}
                </div>}
            </div>;
  })}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    gap: '0.75rem',
    marginTop: '1rem',
    borderTop: '1px solid ' + bd + '0.1)',
    paddingTop: '1rem'
  }}>
          <button type="button" onClick={handleBack} disabled={currentStep <= 1} style={{
    ...btnSecondary,
    ...currentStep <= 1 ? {
      cursor: 'not-allowed',
      opacity: 0.5
    } : {}
  }}>Back</button>
          <button type="button" onClick={handleNext} disabled={currentStep >= totalSteps || !canProceed(currentStep)} style={{
    ...btnPrimary,
    ...currentStep >= totalSteps || !canProceed(currentStep) ? {
      cursor: 'not-allowed',
      opacity: 0.5
    } : {}
  }}>Next</button>
        </div>
      </div>
    </div>;
};

Coding Agent Governance gives security and engineering teams visibility and control over AI coding agents across developer workstations and cloud environments. It inventories the agents, models, MCP servers, and skills in use. It monitors agent actions such as commands, file access, and tool calls, and enforces policies to block, alert on, or require approval for risky behavior before it executes.

Secure coding checks extend these controls with detection backed by Endor Labs intelligence. The malware check looks up packages in the Endor Labs malware feed, and the secrets check scans content the agent writes or reads. See [Secure coding checks](/agent-governance/secure-coding) for more information.

These controls are guardrails for accidental agent behavior, not a sandbox or hard security boundary. See [How Coding Agent Governance works](/agent-governance/how-it-works) for what that means in practice and how events flow into Endor Labs.

<Warning>
  Keep three operating caveats in mind when you rely on Coding Agent Governance:

  * **Enforcement fails open.** If a hook cannot run, or a malware policy cannot reach Endor Labs for a verdict, the action proceeds, and the event is missing from inventory and policy violations until the cause is fixed. An unreadable policy cache still enforces the built-in system policies. See [Limitations](/agent-governance/how-it-works#limitations-of-coding-agent-governance).
  * **Policy changes apply at the next session start.** Hooks evaluate against the policy snapshot fetched when a session starts, so tell developers to restart the agent after a policy edit. See [Local policy cache](/agent-governance/policies#local-policy-cache).
  * **Redaction is pattern-based.** The redactor masks secret-named keys and known credential formats before an event leaves the machine, but a secret in an unknown format can still reach the backend. See [Data sent to Endor Labs](/agent-governance/how-it-works#data-sent-to-endor-labs).
</Warning>

## Supported agents and platforms

Coding Agent Governance enforces policies for supported agents on developer machines. Endor Labs discovers other AI coding tools at session start but does not govern them.

<YamlTable>
  {`
    - Agent and form factor: Claude Code (App and CLI)
    Supported: -yes-
    Platforms: macOS, Linux, Windows
    - Agent and form factor: Claude Cowork¹
    Supported: -no-
    Platforms: macOS, Linux, Windows
    - Agent and form factor: Claude Cloud²
    Supported: -yes-
    Platforms: Hosted sandbox
    - Agent and form factor: Cursor (IDE)
    Supported: -yes-
    Platforms: macOS, Linux, Windows
    - Agent and form factor: Cursor Cloud³
    Supported: -limited-
    Platforms: Linux
    - Agent and form factor: Cursor CLI⁴
    Supported: -limited-
    Platforms: macOS, Linux
    - Agent and form factor: Codex (CLI)
    Supported: -yes-
    Platforms: macOS, Linux, Windows
    - Agent and form factor: GitHub Copilot (CLI)
    Supported: -yes-
    Platforms: macOS, Linux, Windows
    - Agent and form factor: GitHub Copilot (VS Code agent mode)
    Supported: -yes-
    Platforms: macOS, Linux, Windows
    `}
</YamlTable>

<Warning>
  ¹ **Claude Cowork**: Not supported while [claude-code issue #40495](https://github.com/anthropics/claude-code/issues/40495) is open. The Cowork sandbox ignores user settings, managed settings, and environment variables, so governance hooks never load and Cowork sessions are neither monitored nor governed. The failure is silent. Use Claude Code (app or CLI) for governed work.

  ² **Claude Cloud**: Hooks run inside the hosted sandbox and fire from session start.

  ³ **Cursor Cloud**: Agent lifecycle events, such as stop and agent response, do not fire.

  ⁴ **Cursor CLI**: Shell command hooks fire reliably. Prompt, file edit, and stop events do not. On Windows, run the CLI under WSL.
</Warning>

## Set up

<Steps>
  <Step title="Complete the prerequisites">
    Confirm tenant access, check license eligibility, and create an API key with the **AI Audit User** role. See [Prerequisites](/agent-governance/prerequisites).
  </Step>

  <Step title="Deploy hooks">
    Install hooks for [Cursor](/agent-governance/cursor), [Claude Code](/agent-governance/claude-code), [Codex](/agent-governance/codex), and [GitHub Copilot](/agent-governance/copilot). To roll them out across a fleet, see [MDM deployment](/agent-governance/mdm-deployment) and build each configuration with the [in-browser generator](/agent-governance/mdm-deployment#generate-a-configuration-in-your-browser).
  </Step>
</Steps>

## Operate

<Steps>
  <Step title="Review your inventory">
    Find the agents, MCP servers, models, and skills your developers use, each rated with an [Endor Score](/agent-governance/endor-score). See [Review your agent inventory](/agent-governance/inventory).
  </Step>

  <Step title="Triage policy violations">
    Filter, read, and respond to violations from your governed agents as they arrive. See [Triage policy violations](/agent-governance/policy-violations).
  </Step>

  <Step title="Review and extend policies">
    Endor Labs ships an in-built policy catalog enabled by default. Review it, then add custom rules for what to block, alert on, or ask permission for. See [Write a policy](/agent-governance/policies).
  </Step>
</Steps>

## Plan your rollout

Pick your AI coding agent and rollout style. The wizard returns a tailored checklist with deep links to every page you need.

<SetupWizard configUrl="/snippets/setup-wizard-agent-governance.json" />

## Names you'll see

Each name is tied to a specific surface of the same capability:

* **Coding Agent Governance** is the feature name, used across this documentation.
* **Agent Governance** is the name in the Endor Labs user interface, on the left-sidebar entry and on the policy tab under **Policies & Rules**.
* **AI Audit** is the machinery hooks run as. It appears in the **AI Audit User** role for hook API keys, the [endorctl ai-audit](/developers-api/cli/commands/ai-audit) command, and the `ENDOR_AI_AUDIT_*` environment variables.

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Is Coding Agent Governance a security boundary?">
    No. It puts guardrails around agent behavior, not a sandbox, firewall, or hardened boundary. Policy controls lower the risk and blast radius of agent mistakes, but hard guarantees require isolation at the operating system, system call, or sandbox level, which Coding Agent Governance works alongside. See [What Coding Agent Governance is (and isn't)](/agent-governance/how-it-works#what-coding-agent-governance-is-and-isnt).
  </Accordion>

  <Accordion title="Will it stop a determined user from getting around it?">
    No, and that is by design. It targets accidental agent behavior, not a person deliberately trying to defeat the controls. See [What Coding Agent Governance is (and isn't)](/agent-governance/how-it-works#what-coding-agent-governance-is-and-isnt).
  </Accordion>

  <Accordion title="What happens when a policy blocks an action?">
    The agent receives a deny response explaining that a governance policy blocked the action and telling it not to work around the block. The event appears in **Policy Violations**. See [Hooks and what they govern](/agent-governance/how-it-works#hooks-and-what-they-govern).
  </Accordion>

  <Accordion title="Why did a blocked action succeed in a different form?">
    Policies match commands and patterns, not the end effect, so an agent can occasionally reach the same result another way. That trade-off comes with best-effort, pattern-based controls. See [Limitations](/agent-governance/how-it-works#limitations-of-coding-agent-governance).
  </Accordion>

  <Accordion title="What actions can a policy take?">
    **Block**, **Alert** (record only), or **Ask Permission**, written as blocklists or allowlists over shell commands, file access, and MCP tool calls, plus secure coding checks for malware and secrets. See [Write a policy](/agent-governance/policies).
  </Accordion>

  <Accordion title="Can policies catch malicious packages and leaked secrets?">
    Yes. The malware check stops installs of packages flagged by the Endor Labs malware feed, and the secrets check scans content the agent writes or reads on the developer's machine. A secrets violation reports the rule and location, never the secret value. See [Secure coding checks](/agent-governance/secure-coding).
  </Accordion>

  <Accordion title="Can I change the built-in system policies?">
    Yes. Override a system policy with one of the same name and a different action, turn it off in the policy list, or author your own from the same templates. See [Write a policy](/agent-governance/policies).
  </Accordion>

  <Accordion title="How is sensitive data handled?">
    Every command line, file path, URL, environment value, and MCP tool input or output passes through a redactor on the developer's machine before the hook sends anything. Secret-named keys and common credential formats become `[REDACTED]`. Prompt text stays local. See [Data sent to Endor Labs](/agent-governance/how-it-works#data-sent-to-endor-labs).
  </Accordion>

  <Accordion title="Are skills and plugins checked for security?">
    Endor Labs discovers skills at session start and rates them with an [Endor Score](/agent-governance/endor-score), and policies can flag or block suspicious ones. It assesses plugins through the MCP servers and skills they contribute, not as a single unit. See [Skills](/agent-governance/inventory#skills).
  </Accordion>
</AccordionGroup>
