Mount the agent studio. Don't hand-build it.
<AgentStudio> is a themeable, host-wired admin surface for editing agent config in the database, the companion to the DB-driven agent-config surface. Mount it and an operator gets the roster, a full editor for every editable field, tool and skill grants, audiences, and version history with a field-level diff and CAS-safe publish and rollback. No bespoke list, editor, or version UI to build.
Experimental. The @nightowlsdev/react/studio subpath may change shape in minor releases until it stabilizes. The package root and every other subpath keep normal semver discipline; nothing here touches them.
What the studio edits
The editor covers every editable field of an agent version: persona (markdown with a live preview), model, delegates, tool and skill grants over the capability catalog, memory, rules, and workflow. Audiences (who can reach the agent) are edited separately, because they ride the head row rather than the version axis, so no publish or rollback can silently widen exposure. Saving a field publishes a new immutable version; audiences save through their own write.
Mount it, in one import
Pass a client. createEndpointsClient derives the whole route family from one base (any single route can be overridden), and getToken is the same bearer seam the chat provider uses.
import { AgentStudio, createEndpointsClient } from "@nightowlsdev/react/studio";
// One import mounts the whole studio. createEndpointsClient derives the FR-032 route family from a single
// base; getToken is the same bearer seam as <SwarmProvider>. theme is the usual theme system (name / object).
export function AdminAgents({ getToken }: { getToken: () => Promise<string | null> }) {
return <AgentStudio client={createEndpointsClient({ base: "/api/swarm", getToken })} theme="ink" />;
}Wire the routes: authorization is server-side
The client targets six admin routes. Mount them with @nightowlsdev/runner-nextjs, all behind guardDefinitionAdmin.
// app/api/swarm/agent-config/route.ts
// The runner is createNextjsRunner({ …, guardDefinitionAdmin }); the admin plane is DEFAULT-DENY:
// leave guardDefinitionAdmin unset and every route below returns 403.
export const { GET } = runner.configRoute(); // roster + one agent's full detail
export const { POST } = runner.publishRoute(); // CAS-guarded, no-op-aware publish
// The rest of the family, one route file each (all behind guardDefinitionAdmin):
// agent-rollback → runner.rollbackRoute()
// agent-versions → runner.versionsRoute()
// capability-catalog → runner.catalogRoute()
// agent-audiences → runner.audiencesRoute() // FR-031 head-row exposure, never a version publishThe component is untrusted UI. Every write is authorized server-side by guardDefinitionAdmin (default-deny). The studio never constructs an actor and never sends tenant or user ids from the browser. Identity is derived server-side from the request, exactly as the chat provider does. The read-only affordances below are UX only, never the enforcement boundary.
Version history, diff, and the red path
Before you commit a change, the studio surfaces a field-level diff so it is never a blind act: a dirty draft exposes a diff preview beside Publish, and the rollback confirmation always shows the diff of exactly what reverting does. Publishing is CAS-protected: if the head moved under you, it surfaces the conflict with reload or overwrite rather than clobbering someone else's version. And when a change weakens governance (a rule removed, an enforce level downgraded, a match or scope narrowed, a strict workflow dropped, or an approval step lost), it renders as a loud red path: the weakening banner stays visible even with the preview collapsed, and a destructive rollback requires re-typing the agent slug to confirm. Rollback is append-only (it republishes a prior snapshot as a new head, a git revert rather than a reset), and it never changes audiences.
Theme, compose, and degrade gracefully
Theme it with the same system as SwarmChat, override any built-in slot through its own StudioComponents registry (the chat components are untouched), and override any string for copy or light i18n.
// Override any built-in slot or string; theme with the same system as SwarmChat.
<AgentStudio
client={client}
theme="ink"
strings={{ rosterHeading: "Team" }}
components={{ PersonaEditor: MyPersonaEditor }}
/>;
// No runner routes? Pass any object implementing AgentStudioClient in place of createEndpointsClient(...).
// The studio only ever talks to that interface.- No admin grant, canWrite: false, or readOnly → the whole studio renders read-only; a per-agent writable: false (a bundle-managed agent) locks just that agent.
- No capability catalog → the grant picker falls back to read-only chips; a degraded catalog shows a “catalog incomplete” banner.
- No model or audience enumeration → those pickers become free text, with the model input carrying a not-validated warning.
- Rules and workflow sections gate on the active engine's capabilities, with an unenforced-honesty banner when the engine can't enforce them.
Visual rule and workflow builders
As of @nightowlsdev/react@2.7.0, the default RuleBuilder and WorkflowBuilder slots are visual editors. The rule builder gives you condition pickers over a rule's when (agent, tool, and model multiselects fed from the swarm), a deny or ask action, an explicit tool-versus-generation seam control, a normalized preview, and inline validation that mirrors defineRule. The workflow builder gives you ordered step cards (agent, tool, and human steps with {$ref} helpers, branching, and onError) plus a read-only SVG DAG (WorkflowDag) that flags unreachable steps as advisory and dangling targets as errors. Loading an existing rule or workflow into the visual editor and saving it back is round-trip lossless, so you can switch an agent between the visual and JSON editors freely. The builders are client-only (the engine wall holds); the authoritative validation stays server-side.
The JSON editors stay exported as an escape hatch. Alongside the visual defaults, the new RuleBuilderVisual, WorkflowBuilderVisual, WorkflowDag, and useStudioFeeds exports let a host bring its own slot that still reads the pickers' feeds, or render the DAG on its own.
// Visual by default as of @nightowlsdev/react@2.7.0. Keep the JSON editors by passing them as slots:
import { JsonRulesEditor, JsonWorkflowEditor } from "@nightowlsdev/react/studio";
<AgentStudio
client={client}
components={{ RuleBuilder: JsonRulesEditor, WorkflowBuilder: JsonWorkflowEditor }}
/>;
// A custom slot still gets the pickers' data from the feeds seam:
import { useStudioFeeds } from "@nightowlsdev/react/studio";
function MyRuleSlot(props) {
const feeds = useStudioFeeds(); // swarm roster, capability catalog, model allow-list
// render your own control, save through props
}
// The read-only workflow DAG is usable on its own:
<WorkflowDag workflow={workflow} strings={strings} />;