* docs: add llms architecture spec and Layer 1 plan Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): scaffold plugin module, types, and deps * test(llms-txt): cover empty sections guard; drop no-op assignment * fix(llms-txt): restore type-narrowing sections assignment The sections re-assignment in normalizeOptions is load-bearing for the Required<Pick<...>> return type, not a no-op. Documented its purpose. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): add markdown file discovery * fix(llms-txt): move test partial next to its importer page-two.mdx imports ./_partial.mdx, which must resolve to a sibling in topic-b/ for Task 5's partial-inlining test to pass. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): parse docs into title/description/path records * test(llms-txt): assert parseDocFile non-null to satisfy strict null checks Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * test(llms-txt): cover draft-null and heading-fallback in parseDocFile * feat(llms-txt): resolve page URLs from Docusaurus routes * feat(llms-txt): clean MDX into Markdown (partials, directives) Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * fix(llms-txt): make cleanMdxToMarkdown non-throwing on bad frontmatter Move parseFileContentFrontMatter and inlinePartials inside the try block so malformed YAML frontmatter is caught and handled by the regex fallback instead of propagating an unhandled exception. Strip leading frontmatter in the regex fallback path and add a regression test for the contract. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): generate grouped llms.txt index Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * test(llms-txt): cover flat index path and buildHeader directly Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): generate llms-full.txt and per-page payloads Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): generate per-group third-level indexes * test(llms-txt): cover ungrouped-skip and symmetric group isolation * feat(llms-txt): orchestrate generation in postBuild * feat(llms-txt): enable on the docs build Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * fix(llms-txt): strip admonition fences from .md payload, code-block-safe Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * fix(llms-txt): track fence char so nested code fences don't break admonition stripping Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * feat(llms-txt): enable on the integrations build with category grouping Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * fix(llms-txt): key per-group index by slug, display category label Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * fix(llms-txt): unescape Markdown-escaped underscores in partial import paths Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * docs: sync llms-txt plan with implemented test fixtures Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * Tidy names. * docs: redesign Layer 3 around code-mode (search + execute/execute_write) Replace the tool-per-endpoint MCP design with a code-mode server: search over the live OpenAPI schema + a sandboxed execute/execute_write writing against an authenticated ak.request() helper. Document local v1 (binding-is-the-boundary) and the authentik-native OAuth v2 (RBAC-as-authorization, event-log audit). Reconcile Layer 2 with the shipped ak-admin/ak-dev plugins and the L2->L1/L2->L3 seams. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * docs: add Layer 3 v1 code-mode MCP server plan Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * Tidy naming. * Use constants. * fix(llms-txt): make docs build idempotent + quiet logging + index homepage Root cause of the build failures: the docs content root is path:'.', which includes build/. The plugin writes per-page .md into build/, so a second build (dirty build/) re-ingested them as source -> 'Cannot find module @site/build/*.md' (hundreds of fatal errors). Exclude build/ and out/ from the content scan. Also: collapse the per-page 'no route'/'MDX fallback' warnings (dozens of lines that read as errors) into one summary line; and resolve the root index page to '/' (was dropped) with applyMdExtension emitting /index.md for the homepage. Co-Authored-By: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> * Format. * feat(llms-txt): integrations overview + cleaner descriptions (Dominic feedback) - overviewPages option: inline index + applications content as an '## Overview' section in the root llms.txt instead of listing them as links. - Drop the scaffold template/ from integrations (ignoreFiles). - Clean descriptions: strip blockquote '>' markers and trailing '-- <source>' attribution that many integration pages use for their intro citation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * spellcheck. * website: improve llms.txt index quality Address documentation feedback on the generated llms.txt: - Normalize section headings: top-level docs topics get display labels (docs/topics.mjs), and groupLabel title-cases any unmapped slug (e.g. "sys-mgmt" -> "System Management"). - Split the glossary out of "Core Concepts" into its own "Glossary" section via a new `regroup` option. - Skip CVE "Reported by" attributions when extracting descriptions so the summary prose is used instead of the reporter's name. - Clean extracted descriptions: drop bullet-list lead blocks, strip inline links/emphasis/code to plain text, and keep the first sentence. - Resolve generated link URLs from the Netlify deploy context (DEPLOY_PRIME_URL) in previews instead of hardcoding the production subdomain. Tests and fixtures cover the new extraction, grouping, labeling, and deploy-preview URL behavior. * Format. --------- Co-authored-by: Playpen Agent <279763771+playpen-agent@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 KiB
authentik agent-friendly docs + agent architecture
- Status: Design — Layer 1 shipped (PR #23360); Layer 3 redesigned around code-mode
- Date: 2026-06-24
- Revised: 2026-06-24 — Layer 3 reworked from a tool-per-endpoint MCP to code-mode (see "Reviewed with").
- Branch:
agent-friendly-docs - Spans repos:
authentikmonorepo (website/) for Layer 1;authentik-agent-marketplacefor Layers 2–3 - Reviewed with: DeepSeek (3-turn structural consult, session
019ef79b…); the code-mode literature — Cloudflare code-mode, code-mode-mcp, enterprise-mcp, and Ronacher MCPs need code, not 30 tools.
Goal
Let AI coding agents (Claude Code, Cursor) reliably help authentik admins by preferring live retrieval over training data — authentik changes significantly between releases, so an agent's pretrained knowledge is routinely stale. We achieve this with three layers, each fed by an artifact authentik regenerates every release, so steady-state maintenance trends toward zero:
authentik (source of truth, regenerated per release)
├── website MDX (docs + integrations) ─► Layer 1 (llms.txt + .md)
└── /api/v3/schema/ (OpenAPI) ──────────► Layer 3 search()
│
Agent ◄── Layer 2 (skills: "learn from docs, then write code") ┘
│
└──────► Layer 3 (code-mode MCP: search the spec, write `ak.request(...)`
in a sandbox to observe / guarded-act on a live instance)
Both retrieval surfaces follow the same shape: hand the agent a cheap index
(docs llms.txt; API schema.yml) and let it fetch/execute on demand
rather than front-loading content or tools into context.
Question routing:
- Doc questions ("How do I configure SAML?", "App vs Provider?") → L1 + L2, no instance.
- Instance questions ("show me the last 10 failed logins", "which version am I running?") → L3.
- Hybrid / action questions ("add a captcha to my login flow", "reset my admin password") → L2 reads docs to understand the concept, L3 writes code to act.
Non-goals (YAGNI)
- No master root
llms.txton a third domain (docs/integrations cross-link instead). - No precomputed docs→API mapping registry (code-mode's
searchover the live spec subsumes it — see Layer 3). - No tool-per-endpoint MCP. authentik's API is hundreds of endpoints; exposing each as a tool floods context and rots every release. Code-mode keeps a fixed ~3-tool surface regardless of API size.
- No adoption/fork of the community
authentik-mcp(raw HTTP, hand-written tool-per-endpoint, 4× duplication, no guards — high per-release burden, and the wrong shape).
Layer 1 — Docusaurus llms.txt plugin
Ported (leaner) from docusaurus-plugin-llms into the shared theme package.
Location & packaging
New docusaurus-theme/llms-txt/ directory, sibling to releases/ and
redirects/, shipped as source (no build step), matching the existing pattern:
plugin.mjs— Docusaurus plugin factory (default export)node.mjs— file discovery, MDX resolution, generation logiccommon.mjs— option/data types- Add
"./llms-txt/plugin","./llms-txt/node","./llms-txt/common"topackage.jsonexports.
Hook
postBuild — gives final resolved route URLs (routesPaths). This deviates
from the loadContent/contentLoaded pattern the theme's other plugins use,
and that is intentional: accurate final URLs require the post-build phase.
Outputs (three-level "index of indexes")
Per the llmstxt.org convention, for each of the two sites (separate
subdomains docs.goauthentik.io, integrations.goauthentik.io):
/llms.txt— grouped root index. Opens with a cross-link header pointing at the sibling site's/llms.txt. Groups by topic (docs) / category (integrations, driven by the existingcategories.mjs).<dir>/llms.txt— per-topic / per-category index (e.g.integrations.goauthentik.io/cloud-providers/llms.txt,docs.goauthentik.io/add-secure-apps/llms.txt), indexing just that subtree, cross-linking up to the parent index./llms-full.txt— full concatenated content for the site. Retained (not redundant): it is the best single payload for RAG-index seeding and bulk/offline download. Cheap once the plugin already walks the site.- Per-page
<page>.md— the last-hop payload (see below). Index links point at these.mdfiles (the llmstxt.org.md-suffix convention).
Per-page .md payload — core, not optional
This is the decisive correction from the design review: without per-page .md,
an agent traverses the full index chain and then has nothing bite-sized to
fetch — only the oversized llms-full.txt or rendered HTML. So per-page .md
emission is core.
authentik's MDX is not plain Markdown — it uses partial imports
(import X from '_shared.mdx') and custom remark directives (:::ak-version,
enterprise/preview/support badges). A raw .mdx copy would leak unresolved
imports (a hard failure — content simply missing) and directive noise. So the
emit step must:
- Obtain the resolved MDX AST before React component injection.
- Inline partial imports (reuse the source plugin's
resolvePartialImportsapproach). - Strip custom directives (drop
:::ak-*/ badge nodes; they are noise to an agent). - Strip frontmatter, then serialize clean Markdown.
JSX that remains is acceptable — modern LLMs parse it; information loss is the real risk, not residual JSX. This is a one-time, build-stable investment.
Wiring
A createLLMSPlugin(options) helper in docusaurus-theme/config.js, called from
both docs/docusaurus.config.esm.mjs and
integrations/docusaurus.config.esm.mjs. Integrations passes its category map;
docs passes topic config.
Ported lean
- Keep: two core generators (index + full), route-based URL resolution, glob ignore, ordering, section/category grouping, partial-import resolution + directive stripping (per above), batch processing.
- Drop: blog inclusion,
pathTransformation(route resolution covers it),customLLMFiles,keepFrontMatter,addPaths/ignorePaths. - Deps:
gray-matter+minimatch(theme already usesfast-glob).
Layer 2 — Marketplace skills (authentik-agent-marketplace)
Already built: two role-split plugins under plugins/, each a set of
skills that are pointers + method, never knowledge dumps:
ak-admin(12 skills) — organized by authentik's object model:concepts,applications,providers,sources,flows-stages,authenticators-mfa,policies-rbac,users-directory,outposts,events-monitoring,troubleshooting,operations.ak-dev(11 skills) — contributing to authentik: dev-environment, backend, frontend, docs, testing, linting, contributing, community, de-slop.
Each skill is name + description + Purpose + "When to invoke" + "Not this
skill". Every skill carries the "prefer retrieval over pre-training, authentik
changes between releases" directive.
The two seams (near-term wiring, tracked separately from this redesign):
- L2 → L1 (docs): each skill points at the stable root entry-point URL only
(
docs.goauthentik.io/llms.txtorintegrations.goauthentik.io/llms.txt) and tells the agent to traverse links dynamically and fetch the page.md. No deep paths baked into skill prose (survives doc reorganization). Thellms.txtURLs from Layer 1 are these skills' entry points. - L2 → L3 (instance): the
ak-adminskills add one steering line — "to inspect or change the live instance, use the code-mode MCP:searchfor the endpoint, then writeak.request(...)inexecute/execute_write. Learn the concept from the docs first."events-monitoring→execute;flows-stages,users-directory, etc. →execute_write.
This single pair of seams is what ties the three layers: docs teach the concept,
code-mode acts. (The .mcp.json registration and a SessionStart deps-install
hook are already scaffolded in the repo for the Layer 3 server.)
Layer 3 — Code-mode MCP server (authentik-agent-marketplace/mcp-servers/)
Instead of exposing authentik's API as N tools, expose its OpenAPI spec for
search and a sandbox for code, and let the agent write code against an
authenticated ak.request(...) helper. This is the code-mode pattern (Cloudflare,
Ronacher): LLMs are far better at writing code than at emitting tool calls, and a
code surface collapses a hundreds-of-endpoints API to a fixed ~3-tool, near-fixed
token footprint that does not grow as the API grows and tracks the running
instance's version for free.
The three tools (shared by v1 and v2)
search(query)— query the resolvedschema.yml(all$refs inlined) and return only the matching operations:method + path + summary + param / request / response schema slices. This is the only output that scales with the API, and it returns slices, never the whole spec. (Replaces the priorfind_endpoint/describe_endpointpair — search returns the param/response schema the agent needs to construct a call.)execute(code)— run the agent's JS/TS in a sandbox whose only capability is a boundak.request(method, path, { query, body })helper. Read-only: rejects any non-GET verb.execute_write(code)— same sandbox, fullak.request(all verbs). Requires per-call confirmation. Because it carries reads too, a mixed find→create→bind chain runs as one confirmed block — code-mode's composability is preserved. The tool name itself signals intent in any audit trail.
Agent loop: search("captcha stage") → reads the endpoints → writes one code
block calling ak.request(...), chaining as needed, in execute or
execute_write.
v1 — local stdio server (the buildable target)
Lives in authentik-agent-marketplace/mcp-servers/code-mode/ (the npm workspace
already scaffolded; .mcp.json + SessionStart deps-install hook already present).
- Config / auth:
AUTHENTIK_URL+AUTHENTIK_TOKENvia env (the deployment model the communityauthentik-mcpvalidated). The token carries the admin's own permissions. - Schema source: fetch
<AUTHENTIK_URL>/api/v3/schema/at startup so discovery always matches the running instance's version, with a vendoredschema.ymlfallback. This is the zero-maintenance property — discovery follows the instance, nothing to regenerate here. - Sandbox — "the binding is the boundary": in-process
node:vm/worker_threadwith globals stripped to justak+console— nofs, no generalfetch.ak.requestis the only egress; inexecuteit is GET-only. Adversarial isolation is intentionally weak because the trust model is an admin running code against their own instance with their own token — the agent could do the same via any curated call, so the real control is the binding (read-only default + write gate), not the VM (per Ronacher). - Write gate:
execute_writetriggers an MCP confirmation (elicitation) before running; on approval the call is write-armed for that invocation only. Everyexecute_writeis logged locally. - Tools exposed:
search,execute,execute_write. Nothing else. @goauthentik/api: not on the critical path —ak.requestis a generic authenticated fetch overschema.ymlpaths. The generated client'sConfiguration/runtime is an optional transport convenience, not the call surface.
v2 — authentik-native OAuth endpoint (designed; built after v1 proves out)
This is where authentik being an identity provider pays off; it maps onto the enterprise-mcp reference architecture almost 1:1.
- Transport: authentik serves a remote MCP endpoint (HTTP/SSE), e.g.
/mcp, in the product. - Auth: the MCP client performs OAuth against authentik itself (it is the OIDC provider) — no API-token handoff. The agent acts as the authenticated user.
- Authorization = authentik's own RBAC.
ak.requestruns server-side under the user's identity, so authentik's existing per-object / role permissions decide what the code may read or write. No new scope system to invent — reuse what authentik already enforces. (An MCP-level OAuth scope can still gateexecute_writeas a coarse on/off.) - Sandbox: server-side, so real isolation is available (an isolate/worker pool), unlike v1's in-process VM.
- Audit: every
execute/execute_writewrites to authentik's existing event log — the same trail theevents-monitoringadmin skill already queries.
Honest caveat: v2 is product/backend work, release-gated, with a real security surface (public-facing code execution behind an IdP). It is a roadmap design, validated by v1 first — do not start it before the v1 captcha pivot passes.
The pivot test: "add a captcha to my login flow"
This single hybrid task exercises the entire L1→L2→L3 loop and is the gate that
validates the architecture before broad build-out. In code-mode terms: the
flows-stages skill explains stages/flows (from the page .md Layer 1 ships) →
the agent calls search("captcha stage", "flow stage binding") → writes one
execute_write block that creates the captcha stage, finds the target flow, and
POSTs the binding — one confirmed call, no per-endpoint tools. MVP can run against
a mock server and a small schema.yml slice.
Build sequencing (dependency-ordered)
- L1 — core indexes + full-text.
postBuildplugin generating/llms.txt,<topic>/llms.txt,/llms-full.txtfor the docs site. Critical path. - 🛑 GATE — index sanity. On a real docs snapshot: every link resolves, groupings correct, full-text contains content.
- L1 — per-page
.mdwith partial resolution + directive stripping; point all index links at the.mdfiles. Critical path. - 🛑 GATE — content quality. 5 varied pages: no leftover imports, no badge cruft, readable Markdown; feed one to an LLM and confirm it can list the steps.
- L2 —
ak-docsskill skeleton. Entry-point URLs + traverse-and-fetch method. Parallelizable once gate 2 fixes the entry-point URLs. - L3 v1 — code-mode server core:
search(overschema.yml) +execute(read-only sandbox) +execute_write(confirmed). Parallelizable with L2; depends only on the spec. On the critical path for the pivot test. - 🛑 GATE — captcha MVP end-to-end (the pivot). Agent follows L2 → reads the
captcha
.md→searches the spec → writes oneexecute_writeblock (mock server OK). On failure, diagnose schema/prose clarity, not tooling. - L1 — integrations subdomain. (Shipped — both sites live in PR #23360.)
- L2 — wire
ak-admin/ak-devskills to thellms.txtentry points (L2→L1) and add the code-mode steering line (L2→L3). Parallelizable. - L3 v2 — authentik-native OAuth endpoint. Only after the v1 pivot passes; product/backend effort, separate spec + plan.
Cross-cutting
- Distribution: Claude Code + Cursor manifests already in the marketplace; the
ak-admin/ak-devplugins serve both. - Security (v1): local stdio keeps the admin's token in their own environment;
the sandbox exposes only
ak(nofs/fetch);executeis GET-only;execute_writeis confirmed per call and logged. The binding is the boundary. - Security (v2): OAuth against authentik; authorization is authentik's own RBAC under the user's identity; every call audited to the event log.
- Maintenance thesis: docs (
.md+ indexes) and the API surface (searchover the liveschema.yml) both come straight from authentik each release; L2 is thin glue and L3 has no per-endpoint code to patch. Steady-state human upkeep approaches zero.
Open questions for plan stage
- L3: how
searchranks/filters operations fromschema.yml(keyword over path+summary+tags; how much schema slice to return per hit without bloating context). - L3: the exact
node:vmvsworker_threadsandbox choice and how globals are stripped to justak+console; how the MCP confirmation (elicitation) forexecute_writeis surfaced across Claude Code / Cursor. - L3:
ak.requesttransport — plain authenticatedfetchvs.@goauthentik/api'sConfigurationruntime; how/api/v3/schema/fetch-at-startup vs. vendored fallback is selected. - L2 wiring: confirm the single steering line per
ak-adminskill and the stablellms.txtentry-point URLs (depends only on Layer 1, already shipped).