// — minimal agent creation form, POSTs to // swarm-controller's `POST /api/agents`. That endpoint only queues a // `CreateIdentity` job and returns its node id — the identity isn't // guaranteed to exist yet when this gets a response — so this shows // the queued confirmation and links to the Jobs page to watch it // settle, rather than duplicating JobqGraph's per-node polling here. // Scope matches the originating issue exactly: name field only, no // forge/deploy options (those aren't wired server-side yet either). // // Rendered inside a `Dialog` from `AgentsPage`'s own "+ agent" button, // not a standalone route — the roster it feeds is the natural home for // the action that populates it, and a distinct top-level nav item next // to it was one click of indirection for no benefit. Kept as its own // component (not inlined into AgentsPage) since the two-panel layout // below is unchanged from its former life as a page: same content, same // review history, just a different mount point. // // `hive` field added because agent creation had no way to record which // hive an agent runs on. `POST /api/agents` now requires it, so the // roster fetched off `GET /api/hives` backs a required select here // rather than a free-text field — a spawn target has to be one of the // swarm's actual hives, same reasoning `Ident.parse` gets client-side // pattern validation for `name`. // // Its name field, hive select, and submit button all come from the // shared `ui/` form kit (`TextField`/`SelectField`/`Button`) rather than // page-scoped input/button chrome — see that kit's own comments for why. // // Permissions: checkboxes from `GET /api/permissions`, on the default set plus // `extraPermissions`; sent only when that differs from the default. import { useEffect, useState } from "preact/hooks"; import { Link } from "wouter-preact"; import { ApiErrorPanel } from "@hive/shared/api-error-panel.js"; import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js"; import { Panel } from "../ui/panel/Panel.js"; import { TextField } from "../ui/text-field/TextField.js"; import { SelectField, type SelectOption, } from "../ui/select-field/SelectField.js"; import { TextAreaField } from "../ui/text-area-field/TextAreaField.js"; import { Button } from "../ui/button/Button.js"; import { capsBody, LimitsFields } from "../agent/detail/AgentLimits.js"; import { draftBody, draftProblem, EMPTY_INFERENCE_DRAFT, InferenceFields, type InferenceDraft, } from "../agent/detail/AgentInference.js"; import { FormField } from "../ui/form-field/FormField.js"; import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js"; import { initialSelection, requestPermissions, togglePermission, } from "./CreateAgentPermissions.js"; import { archetypeKey, prefill, requestArchetype, type ArchetypeList, } from "./CreateAgentArchetype.js"; import { agentsMdBody, agentsMdRefusal } from "./CreateAgentAgentsMd.js"; import "./CreateAgentForm.css"; // Mirrors swarm-controller's `HiveEntry` — same shape `HivesPage` // consumes off `/api/hives/status`, but this page hits the plain // `/api/hives` roster (no status/freshness needed, just "what hives // exist to spawn into"). interface HiveEntry { name: string; domain: string; } interface CreateAgentResponse { node_id: number; } // Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`). interface PermissionCatalogue { all: string[]; default: string[]; } type SubmitState = | { status: "idle" } | { status: "submitting" } | { status: "done"; nodeId: number } | { status: "error"; problem: ProblemDetails }; // Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`) — // a `pattern` hint only, not a substitute for the server's own // validation; a name this rejects would fail server-side anyway, so // catching it before a round-trip is a pure UX win, not a new gate. // // Hyphen escaped (`\-`) rather than left trailing in the class: modern // browsers validate the `pattern` attribute's regex in Unicode-set (`v`) // mode, which is far stricter about hyphen placement than classic mode — // `[a-z0-9-]` throws "Invalid character class" under `v` mode even though // it's valid classic-mode regex (a trailing `-` is unambiguous there). // Reproduced directly: `new RegExp('[a-z0-9-]', 'v')` throws, // `new RegExp('[a-z0-9\\-]', 'v')` doesn't — confirmed via mara's console // exception report on this page. const NAME_PATTERN = "[a-z0-9\\-]{1,63}"; export function CreateAgentForm({ onClose, initialName = "", extraPermissions = [], }: { /** The name field's starting value, for a caller that suggests one. */ initialName?: string; /** Permissions checked on top of the default set when the form opens. */ extraPermissions?: string[]; /** Rendered as the form panel's own close button — this form always * mounts inside a `plain` `Dialog` (see `AgentsPage`), which no longer * floats its own. */ onClose?: () => void; }) { const [name, setName] = useState(initialName); const [hive, setHive] = useState(""); // `null` = still loading, `[]` = loaded but empty (a real, if unusual, // swarm state) — distinct from "not fetched yet" so the placeholder // option's label can tell the two apart. const [hives, setHives] = useState(null); const [hivesError, setHivesError] = useState(null); const [result, setResult] = useState({ status: "idle" }); // The optional settings sections, left empty unless the operator fills them. const [cpu, setCpu] = useState(""); const [memory, setMemory] = useState(""); const [inference, setInference] = useState( EMPTY_INFERENCE_DRAFT, ); const inferenceProblem = draftProblem(inference); // `null` until `/api/permissions` answers; `selected` starts on its default. const [catalogue, setCatalogue] = useState(null); const [catalogueError, setCatalogueError] = useState( null, ); const [selected, setSelected] = useState([]); // `null` until `/api/archetypes` answers; `archetype` is the picked one's // `archetypeKey`, `""` for none. const [archetypes, setArchetypes] = useState(null); const [archetypesError, setArchetypesError] = useState( null, ); const [archetype, setArchetype] = useState(""); // The text the agent's `AGENTS.md` starts with; empty sends none. const [agentsMd, setAgentsMd] = useState(""); const picked = archetypes?.archetypes.find( (a) => archetypeKey(a) === archetype, ); // Loaded once, not re-fetched on submit: the roster changes rarely // enough (a nix-level swarm config change) that staleness within one // page visit isn't worth a second round-trip per keystroke/submit. useEffect(() => { (async () => { const r = await fetch("/api/hives"); if (!r.ok) { setHivesError(await readApiError(r)); return; } const data = (await r.json()) as HiveEntry[]; setHives(data); // Single-hive swarms are the common case — default it rather than // making the operator pick the only option. Multi-hive swarms get // no default (the placeholder stays selected), so `required` on // the select below forces an explicit choice per mara's scope. if (data.length === 1) setHive(data[0].name); })().catch((e: unknown) => setHivesError({ detail: String(e) })); }, []); useEffect(() => { (async () => { const r = await fetch("/api/permissions"); if (!r.ok) { setCatalogueError(await readApiError(r)); return; } const data = (await r.json()) as PermissionCatalogue; setCatalogue(data); setSelected(initialSelection(data.all, data.default, extraPermissions)); })().catch((e: unknown) => setCatalogueError({ detail: String(e) })); }, []); useEffect(() => { (async () => { const r = await fetch("/api/archetypes"); if (!r.ok) { setArchetypesError(await readApiError(r)); return; } setArchetypes((await r.json()) as ArchetypeList); })().catch((e: unknown) => setArchetypesError({ detail: String(e) })); }, []); // Fills permissions, caps and inference from the picked archetype; each // stays editable, and the request carries only those fields. function pickArchetype(key: string) { setArchetype(key); const p = prefill( archetypes?.archetypes.find((a) => archetypeKey(a) === key), catalogue?.default ?? [], ); setSelected(p.permissions); setCpu(p.cpu); setMemory(p.memory); setInference(p.inference); } const hiveOptions: SelectOption[] = (hives ?? []).map((h) => ({ value: h.name, label: h.name, })); const hivesReady = hives !== null && hives.length > 0; // The controller's size refusal of the AGENTS.md text, shown under its // section so a collapsed section still shows it. const refusal = result.status === "error" ? agentsMdRefusal(result.problem) : null; async function submit(e: Event) { e.preventDefault(); setResult({ status: "submitting" }); try { // A section left empty is left out, so the request is the plain // name-and-hive one unless the operator set something. const limits = capsBody(cpu, memory); const settings = draftBody(inference); const r = await fetch("/api/agents", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name, hive, ...(Object.keys(limits).length > 0 ? { limits } : {}), ...(Object.keys(settings).length > 0 ? { inference: settings } : {}), permissions: catalogue ? requestPermissions(selected, catalogue.default) : undefined, archetype: requestArchetype(picked), ...agentsMdBody(agentsMd), }), }); if (!r.ok) { setResult({ status: "error", problem: await readApiError(r) }); return; } const data = (await r.json()) as CreateAgentResponse; setResult({ status: "done", nodeId: data.node_id }); setName(""); setAgentsMd(""); pickArchetype(""); if (catalogue) setSelected( initialSelection(catalogue.all, catalogue.default, extraPermissions), ); } catch (err) { setResult({ status: "error", problem: { detail: String(err) } }); } } return (

Create a new agent's swarm-level identity. This only queues the job — check jobs to watch it settle. Resource caps, inference and instructions are optional; leave them empty to set nothing. A name set up before keeps the settings stored for it.

{hivesError && ( )} {catalogueError && ( )} {archetypesError && ( )}
{archetypes && archetypes.archetypes.length > 0 && ( <> ({ value: archetypeKey(a), label: a.name, })), ]} /> {picked && (

{picked.description} An agent created from an archetype has no config repo: its hive builds it from the archetype's module.

)} )}
{catalogue ? ( setSelected((s) => togglePermission(s, v))} /> ) : ( loading… )}
resource caps
inference
instructions
{refusal !== null && (

{refusal}

)} {result.status === "done" && (

queued as job node #{result.nodeId} —{" "} watch it in jobs

)} {result.status === "error" && refusal === null && ( )}
{/* Describes the intended end state (a running agent), not today's literal behaviour (deploying the container onto the hive isn't wired up server-side yet — see this file's top comment) — mara's explicit call on this copy: read as finished, not as a running commentary on partial implementation. */}

Submitting this queues everything a new agent needs: a swarm-level identity, a config repo on the forge with the operator added as a collaborator, and a container running the agent on the hive you pick above. An agent created from an archetype gets no config repo: its container runs the archetype's module. Watch it all settle on{" "} jobs.

); }