Watch
0
0
Fork
You've already forked hyperhive
0

offer the log mode in the swarm ui

This commit is contained in:
atlas 2026-10-30 02:18:39 +01:00 • committed by mara
commit 9140d063d7
3 changed files with 88 additions and 28 deletions

View file

@ -27,7 +27,9 @@ import {
editorSelection, editorSelection,
grantSummary, grantSummary,
internetBody, internetBody,
internetMode,
internetSummary, internetSummary,
type InternetMode,
} from "./permissions.js"; } from "./permissions.js";
import "./AgentLimits.css"; import "./AgentLimits.css";
@ -35,6 +37,7 @@ import "./AgentLimits.css";
interface Grant { interface Grant {
permissions?: string[]; permissions?: string[];
unrestricted_internet?: boolean; unrestricted_internet?: boolean;
internet_log?: boolean;
} }
// Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`). // Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`).
@ -43,6 +46,17 @@ interface Catalogue {
default: string[]; 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 { function permissionsUrl(agent: string): string {
return `/api/agents/${encodeURIComponent(agent)}/permissions`; return `/api/agents/${encodeURIComponent(agent)}/permissions`;
} }
@ -75,15 +89,15 @@ export function AgentPermissions({ agent }: { agent: string }) {
}; };
}, [agent, reload]); }, [agent, reload]);
// Flips the internet flag, sending the stored list with it. // Sets the internet mode, sending the stored list with it.
async function setInternet(g: Grant, unrestricted: boolean) { async function setInternet(g: Grant, mode: InternetMode) {
if (flagBusy) return; if (flagBusy) return;
setFlagBusy(true); setFlagBusy(true);
try { try {
const r = await fetch(permissionsUrl(agent), { const r = await fetch(permissionsUrl(agent), {
method: "PUT", method: "PUT",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify(internetBody(g.permissions, unrestricted)), body: JSON.stringify(internetBody(g.permissions, mode)),
}); });
if (!r.ok) { if (!r.ok) {
setError(await readApiError(r)); setError(await readApiError(r));
@ -130,20 +144,21 @@ export function AgentPermissions({ agent }: { agent: string }) {
) : null} ) : null}
<Badge <Badge
label="internet" label="internet"
value={internetSummary(grant.unrestricted_internet)} value={internetSummary(grant.unrestricted_internet, grant.internet_log)}
/>
<Badge
variant="quiet"
value={
grant.unrestricted_internet === false
? "allow unrestricted"
: "restrict"
}
title={`set whether ${agent}'s traffic may leave the host unfiltered`}
onClick={() =>
void setInternet(grant, grant.unrestricted_internet === false)
}
/> />
{INTERNET_MODES.filter(
([mode]) =>
mode !==
internetMode(grant.unrestricted_internet, grant.internet_log),
).map(([mode, label, title]) => (
<Badge
key={mode}
variant="quiet"
value={label}
title={`${title} for ${agent}`}
onClick={() => void setInternet(grant, mode)}
/>
))}
<Badge <Badge
variant="quiet" variant="quiet"
value="edit" value="edit"

View file

@ -22,26 +22,50 @@ export function grantSummary(stored: string[] | undefined): string {
return `${stored.length} granted`; return `${stored.length} granted`;
} }
/** The three modes of an agent's internet access. */
export type InternetMode = "unrestricted" | "log" | "restricted";
/** /**
* How the panel reads the stored `unrestricted_internet`. An absent value * How the panel reads the stored `unrestricted_internet` and `internet_log`.
* counts as on, and says "legacy" because the swarm never set it. * An absent flag counts as on, and says "legacy" because the swarm never set
* it. `log` is `internet_log` beside an explicit `false`.
*/ */
export function internetSummary(stored: boolean | undefined): string { export function internetSummary(
stored: boolean | undefined,
log?: boolean,
): string {
if (stored === undefined) return "unrestricted (legacy)"; if (stored === undefined) return "unrestricted (legacy)";
return stored ? "unrestricted" : "restricted"; if (stored) return "unrestricted";
return log ? "log (unlisted names allowed, logged)" : "restricted";
}
/** The mode the stored fields put the agent in. */
export function internetMode(
stored: boolean | undefined,
log?: boolean,
): InternetMode {
if (stored === undefined || stored) return "unrestricted";
return log ? "log" : "restricted";
} }
/** /**
* The `PUT` body that sets the internet flag. A `PUT` without `permissions` * The `PUT` body that sets the internet mode. A `PUT` without `permissions`
* resets a stored list to the default, so the stored list goes along; an * resets a stored list to the default, so the stored list goes along; an
* agent with none stored sends the flag alone, which leaves it unset. * agent with none stored sends the mode alone, which leaves the list unset.
* `log` names `internet_log` beside `unrestricted_internet: false`; the other
* two leave it out, which ends logging.
*/ */
export function internetBody( export function internetBody(
stored: string[] | undefined, stored: string[] | undefined,
unrestricted: boolean, mode: InternetMode,
): { permissions?: string[]; unrestricted_internet: boolean } { ): {
permissions?: string[];
unrestricted_internet: boolean;
internet_log?: true;
} {
return { return {
...(stored === undefined ? {} : { permissions: stored }), ...(stored === undefined ? {} : { permissions: stored }),
unrestricted_internet: unrestricted, unrestricted_internet: mode === "unrestricted",
...(mode === "log" ? { internet_log: true as const } : {}),
}; };
} }

View file

@ -6,6 +6,7 @@ import {
editorSelection, editorSelection,
grantSummary, grantSummary,
internetBody, internetBody,
internetMode,
internetSummary, internetSummary,
} from "../src/agent/detail/permissions.ts"; } from "../src/agent/detail/permissions.ts";
@ -43,18 +44,38 @@ test("an absent internet flag reads as unrestricted, marked legacy", () => {
}); });
test("the internet toggle sends the stored list with the flag", () => { 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"], permissions: ["meta", "inbox"],
unrestricted_internet: false, unrestricted_internet: false,
}); });
assert.deepEqual(internetBody([], true), { assert.deepEqual(internetBody([], "unrestricted"), {
permissions: [], permissions: [],
unrestricted_internet: true, unrestricted_internet: true,
}); });
}); });
test("with no stored list the toggle leaves the list unset", () => { test("with no stored list the toggle leaves the list unset", () => {
assert.deepEqual(internetBody(undefined, false), { assert.deepEqual(internetBody(undefined, "restricted"), {
unrestricted_internet: false, 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);
});