From 10508895c4f472f9134b288eed1f87b3e8c3fec5 Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 27 Oct 2026 13:38:58 +0100 Subject: [PATCH] swarm-ui: show and toggle an agent's unrestricted internet flag, add it to the create dialog --- docs/swarm/ui.md | 11 ++++- .../src/agent/detail/AgentPermissions.tsx | 47 ++++++++++++++++++- .../swarm-ui/src/agent/detail/permissions.ts | 24 ++++++++++ .../swarm-ui/src/pages/CreateAgentForm.tsx | 12 +++++ .../src/pages/CreateAgentPermissions.ts | 10 ++++ .../swarm-ui/test/agent-permissions.test.mjs | 25 ++++++++++ .../test/create-agent-permissions.test.mjs | 9 ++++ 7 files changed, 136 insertions(+), 2 deletions(-) diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index a0f39470..feaedec1 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -562,6 +562,14 @@ ignores a value it can't parse and the agent would run uncapped, and it refuses, for example, a percentage with a leading zero. The dialog shows the refusal's reason under the fields and leaves them as you typed them. +Below the list, **internet** shows whether the agent's traffic may leave the +host unfiltered: `unrestricted`, `restricted`, or `unrestricted (legacy)` when +the swarm never set the flag, which counts as on. The **restrict** or +**allow unrestricted** badge flips it with a `PUT` that carries the stored +permission list along with the flag, because a `PUT` without the list resets a +stored list to the default. An agent with no stored list sends the flag alone. +Nothing acts on the flag yet. + A saved change isn't instant. The hive reads the caps about every 30 seconds, and a running agent takes a changed cap at its next spawn or rebuild, so the row shows the new value before the agent runs under it. The panel reads @@ -605,7 +613,8 @@ order with **next** and can step back through with **back** or the step tabs: 2. **instructions**: the text the agent's `AGENTS.md` starts with, in a box of its own. 3. **permissions**: one checkbox per permission, starting from the default set - or the archetype's. + or the archetype's, and an **Unrestricted internet** checkbox, unticked by + default. Ticking it adds `unrestricted_internet` to the request. 4. **resources and inference**: the optional resource caps and inference settings. 5. **summary**: every setting the request will carry, and what stays at its diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx index 883030c9..03de607a 100644 --- a/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx +++ b/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx @@ -23,12 +23,18 @@ import { requestPermissions, togglePermission, } from "../../pages/CreateAgentPermissions.js"; -import { editorSelection, grantSummary } from "./permissions.js"; +import { + editorSelection, + grantSummary, + internetBody, + internetSummary, +} from "./permissions.js"; import "./AgentLimits.css"; // The stored grant as the route carries it; the field is absent when unset. interface Grant { permissions?: string[]; + unrestricted_internet?: boolean; } // Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`). @@ -47,6 +53,7 @@ export function AgentPermissions({ agent }: { agent: string }) { // Bumped after a save or a clear, so the grant is fetched again. const [reload, setReload] = useState(0); const [editing, setEditing] = useState(false); + const [flagBusy, setFlagBusy] = useState(false); useEffect(() => { let cancelled = false; @@ -68,6 +75,28 @@ export function AgentPermissions({ agent }: { agent: string }) { }; }, [agent, reload]); + // Flips the internet flag, sending the stored list with it. + async function setInternet(g: Grant, unrestricted: boolean) { + if (flagBusy) return; + setFlagBusy(true); + try { + const r = await fetch(permissionsUrl(agent), { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify(internetBody(g.permissions, unrestricted)), + }); + if (!r.ok) { + setError(await readApiError(r)); + return; + } + setReload((n) => n + 1); + } catch (e: unknown) { + setError({ detail: String(e) }); + } finally { + setFlagBusy(false); + } + } + // A grant already loaded stays under a failed refetch, so a save that // worked is not hidden by a read that did not. const errorPanel = error ? ( @@ -99,6 +128,22 @@ export function AgentPermissions({ agent }: { agent: string }) { ))} ) : null} + + + void setInternet(grant, grant.unrestricted_internet === false) + } + /> ([]); + // Unticked: the server writes `false`, so the agent starts restricted. + const [internet, setInternet] = useState(false); // `null` until `/api/archetypes` answers; `archetype` is the picked one's // `archetypeKey`, `""` for none. const [archetypes, setArchetypes] = useState(null); @@ -231,6 +234,7 @@ export function CreateAgentForm({ permissions: catalogue ? requestPermissions(selected, catalogue.default) : undefined, + ...requestInternet(internet), archetype: requestArchetype(picked), ...agentsMdBody(agentsMd), }), @@ -434,6 +438,14 @@ export function CreateAgentForm({ )} + )} {!done && step === "resources" && ( diff --git a/frontend/packages/swarm-ui/src/pages/CreateAgentPermissions.ts b/frontend/packages/swarm-ui/src/pages/CreateAgentPermissions.ts index be62c338..9520538d 100644 --- a/frontend/packages/swarm-ui/src/pages/CreateAgentPermissions.ts +++ b/frontend/packages/swarm-ui/src/pages/CreateAgentPermissions.ts @@ -38,3 +38,13 @@ export function initialSelection( ...extra.filter((p) => all.includes(p) && !defaults.includes(p)), ]; } + +/** + * The create request's internet field: `unrestricted_internet: true` while + * the box is ticked, nothing otherwise, as the server writes `false` for it. + */ +export function requestInternet(ticked: boolean): { + unrestricted_internet?: true; +} { + return ticked ? { unrestricted_internet: true } : {}; +} diff --git a/frontend/packages/swarm-ui/test/agent-permissions.test.mjs b/frontend/packages/swarm-ui/test/agent-permissions.test.mjs index a607d8d6..56a6a96e 100644 --- a/frontend/packages/swarm-ui/test/agent-permissions.test.mjs +++ b/frontend/packages/swarm-ui/test/agent-permissions.test.mjs @@ -5,6 +5,8 @@ import { test } from "node:test"; import { editorSelection, grantSummary, + internetBody, + internetSummary, } from "../src/agent/detail/permissions.ts"; const all = ["messaging", "meta", "inbox", "execution", "manage_root_agent"]; @@ -33,3 +35,26 @@ test("the summary tells unset, empty and a list apart", () => { assert.equal(grantSummary([]), "none"); assert.equal(grantSummary(["meta", "inbox"]), "2 granted"); }); + +test("an absent internet flag reads as unrestricted, marked legacy", () => { + assert.equal(internetSummary(undefined), "unrestricted (legacy)"); + assert.equal(internetSummary(true), "unrestricted"); + assert.equal(internetSummary(false), "restricted"); +}); + +test("the internet toggle sends the stored list with the flag", () => { + assert.deepEqual(internetBody(["meta", "inbox"], false), { + permissions: ["meta", "inbox"], + unrestricted_internet: false, + }); + assert.deepEqual(internetBody([], true), { + permissions: [], + unrestricted_internet: true, + }); +}); + +test("with no stored list the toggle leaves the list unset", () => { + assert.deepEqual(internetBody(undefined, false), { + unrestricted_internet: false, + }); +}); diff --git a/frontend/packages/swarm-ui/test/create-agent-permissions.test.mjs b/frontend/packages/swarm-ui/test/create-agent-permissions.test.mjs index 07109b23..f7ec493b 100644 --- a/frontend/packages/swarm-ui/test/create-agent-permissions.test.mjs +++ b/frontend/packages/swarm-ui/test/create-agent-permissions.test.mjs @@ -4,6 +4,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { initialSelection, + requestInternet, requestPermissions, togglePermission, } from "../src/pages/CreateAgentPermissions.ts"; @@ -44,3 +45,11 @@ test("the starting selection is the default plus the known extras", () => { defaults, ); }); + +test("the create box is unticked by default and sends nothing", () => { + assert.deepEqual(requestInternet(false), {}); +}); + +test("a ticked create box sends the flag", () => { + assert.deepEqual(requestInternet(true), { unrestricted_internet: true }); +});