diff --git a/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx b/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx
index 03de607a..379a88c7 100644
--- a/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx
+++ b/frontend/packages/swarm-ui/src/agent/detail/AgentPermissions.tsx
@@ -27,7 +27,9 @@ import {
editorSelection,
grantSummary,
internetBody,
+ internetMode,
internetSummary,
+ type InternetMode,
} from "./permissions.js";
import "./AgentLimits.css";
@@ -35,6 +37,7 @@ import "./AgentLimits.css";
interface Grant {
permissions?: string[];
unrestricted_internet?: boolean;
+ internet_log?: boolean;
}
// Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`).
@@ -43,6 +46,17 @@ interface Catalogue {
default: string[];
}
+// The buttons for the modes an agent is not in: mode, label, tooltip.
+const INTERNET_MODES: [InternetMode, string, string][] = [
+ [
+ "unrestricted",
+ "allow unrestricted",
+ "let its traffic leave the host unfiltered",
+ ],
+ ["log", "log", "allow what restricted would deny, and log it as would_deny"],
+ ["restricted", "restrict", "allow only the listed names"],
+];
+
function permissionsUrl(agent: string): string {
return `/api/agents/${encodeURIComponent(agent)}/permissions`;
}
@@ -75,15 +89,15 @@ 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) {
+ // Sets the internet mode, sending the stored list with it.
+ async function setInternet(g: Grant, mode: InternetMode) {
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)),
+ body: JSON.stringify(internetBody(g.permissions, mode)),
});
if (!r.ok) {
setError(await readApiError(r));
@@ -130,20 +144,21 @@ export function AgentPermissions({ agent }: { agent: string }) {
) : null}
-
- void setInternet(grant, grant.unrestricted_internet === false)
- }
+ value={internetSummary(grant.unrestricted_internet, grant.internet_log)}
/>
+ {INTERNET_MODES.filter(
+ ([mode]) =>
+ mode !==
+ internetMode(grant.unrestricted_internet, grant.internet_log),
+ ).map(([mode, label, title]) => (
+ void setInternet(grant, mode)}
+ />
+ ))}
{
});
test("the internet toggle sends the stored list with the flag", () => {
- assert.deepEqual(internetBody(["meta", "inbox"], false), {
+ assert.deepEqual(internetBody(["meta", "inbox"], "restricted"), {
permissions: ["meta", "inbox"],
unrestricted_internet: false,
});
- assert.deepEqual(internetBody([], true), {
+ assert.deepEqual(internetBody([], "unrestricted"), {
permissions: [],
unrestricted_internet: true,
});
});
test("with no stored list the toggle leaves the list unset", () => {
- assert.deepEqual(internetBody(undefined, false), {
+ assert.deepEqual(internetBody(undefined, "restricted"), {
unrestricted_internet: false,
});
});
+
+test("log is internet_log beside an explicit false, and nothing else reads as it", () => {
+ assert.equal(internetMode(false, true), "log");
+ assert.equal(internetMode(false, undefined), "restricted");
+ assert.equal(internetMode(false, false), "restricted");
+ assert.equal(internetMode(true, true), "unrestricted");
+ assert.equal(internetMode(undefined, true), "unrestricted");
+ assert.match(internetSummary(false, true), /^log /);
+ assert.equal(internetSummary(false), "restricted");
+});
+
+test("the log mode keeps the old flag false and names internet_log; the others leave it out", () => {
+ assert.deepEqual(internetBody(["meta"], "log"), {
+ permissions: ["meta"],
+ unrestricted_internet: false,
+ internet_log: true,
+ });
+ assert.equal("internet_log" in internetBody([], "restricted"), false);
+ assert.equal("internet_log" in internetBody([], "unrestricted"), false);
+});