The agent view in the swarm UI gets a message box. The text reaches the
agent's harness straight from swarm-controller over the swarm queue and
becomes an inbox turn from the operator's authelia username. hive-c0re is
not in the path.
- gateway: `/api/` forwards authelia's `Remote-User`, replacing any the
client sent.
- swarm-controller: `POST /api/agents/{name}/input` takes that header as
the sender (401 without it) and sends `{from, body}` as a NATS request
on `$SWARM.input.<agent>`. No subscriber is a 503.
- swarm-nats-auth: the controller is granted publish on `$SWARM.input.*`;
no hive or agent grant names the family, so the harness can trust
`from`.
- hive-agent: a task subscribes to the agent's own input subject and
pushes each message into the serve loop's existing reminder channel,
renamed `local_msg_*` since it now carries both.
The harness does not reply to the request: no agent grant covers a reply
subject. The controller therefore answers 202 once the request times out
(2s) and 503 on the server's no-responders status.
Closes #4828
70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
// <AgentInputForm> — text for one agent, under its swarm terminal.
|
|
// POSTs `/api/agents/{name}/input`: 202 once the agent's harness holds the
|
|
// message, 503 when it is not listening, problem+json otherwise. The agent
|
|
// sees it as an inbox turn from the logged-in user (the controller takes the
|
|
// sender from the gateway, not from this request).
|
|
import { useState } from "preact/hooks";
|
|
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
|
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
|
import { TextField } from "../../ui/text-field/TextField.js";
|
|
import { Button } from "../../ui/button/Button.js";
|
|
import "./AgentInputForm.css";
|
|
|
|
type SubmitState =
|
|
| { status: "idle" }
|
|
| { status: "submitting" }
|
|
| { status: "sent" }
|
|
| { status: "error"; problem: ProblemDetails };
|
|
|
|
export function AgentInputForm({ agentName }: { agentName: string }) {
|
|
const [body, setBody] = useState("");
|
|
const [result, setResult] = useState<SubmitState>({ status: "idle" });
|
|
|
|
async function submit(e: Event) {
|
|
e.preventDefault();
|
|
setResult({ status: "submitting" });
|
|
try {
|
|
const r = await fetch(
|
|
`/api/agents/${encodeURIComponent(agentName)}/input`,
|
|
{
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ body }),
|
|
},
|
|
);
|
|
if (!r.ok) {
|
|
setResult({ status: "error", problem: await readApiError(r) });
|
|
return;
|
|
}
|
|
setResult({ status: "sent" });
|
|
setBody("");
|
|
} catch (err) {
|
|
setResult({ status: "error", problem: { detail: String(err) } });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div class="ui-agent-input">
|
|
<form class="ui-agent-input-form" onSubmit={submit}>
|
|
<TextField
|
|
id={`agent-input-${agentName}`}
|
|
label={`message ${agentName}`}
|
|
value={body}
|
|
required
|
|
onInput={setBody}
|
|
/>
|
|
<Button
|
|
variant="primary"
|
|
type="submit"
|
|
disabled={result.status === "submitting"}
|
|
>
|
|
{result.status === "submitting" ? "sending…" : "send"}
|
|
</Button>
|
|
</form>
|
|
{result.status === "sent" && <p class="ui-agent-input-sent">sent</p>}
|
|
{result.status === "error" && (
|
|
<ApiErrorPanel context="failed to send" problem={result.problem} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|