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
2 changed files with 88 additions and 28 deletions

View file

@ -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}
<Badge
label="internet"
value={internetSummary(grant.unrestricted_internet)}
/>
<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)
}
value={internetSummary(grant.unrestricted_internet, grant.internet_log)}
/>
{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
variant="quiet"
value="edit"

View file

@ -22,26 +22,50 @@ export function grantSummary(stored: string[] | undefined): string {
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
* counts as on, and says "legacy" because the swarm never set it.
* How the panel reads the stored `unrestricted_internet` and `internet_log`.
* 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)";
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
* 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(
stored: string[] | undefined,
unrestricted: boolean,
): { permissions?: string[]; unrestricted_internet: boolean } {
mode: InternetMode,
): {
permissions?: string[];
unrestricted_internet: boolean;
internet_log?: true;
} {
return {
...(stored === undefined ? {} : { permissions: stored }),
unrestricted_internet: unrestricted,
unrestricted_internet: mode === "unrestricted",
...(mode === "log" ? { internet_log: true as const } : {}),
};
}