swarm-ui: the egress card on the swarm page lists and edits the swarm-wide denied domains, and says which hives have not applied them
This commit is contained in:
parent
0bc350a8a1
commit
0421f28d49
7 changed files with 210 additions and 103 deletions
|
|
@ -23,16 +23,16 @@ shut. A dropdown inside a dialog opens above the dialog and the dialog doesn't c
|
|||
A dialog grows to fit its content, up to 64em or nine tenths of the window, whichever is
|
||||
smaller; short confirm dialogs stay short.
|
||||
|
||||
| route | what |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/` | the start page: the swarm's name over a digital-rain backdrop with the slug (`hyperhive / we are the wired`, its shaded gradients pulsing in a wave unless reduced motion is on) at the foot, plus a paperclip offering to set up an agent while no `ruth` exists, or her hive declares her destroyed (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
||||
| `/agents` | every agent as a card that opens in place (status, terminal, subagents, and settings with permissions, internet, allowed domains and accounts); create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||
| `/swarm` | what belongs to the whole swarm: a folded **hives** card with the hive directory and each hive's last reported status, a folded **egress** card with the swarm-wide allowed domains, and a folded, **beta** **notification router** card (with a warning banner) holding the router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows. `/notifications` redirects here |
|
||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||
| `/reports` | two folded cards, each read only once opened: the **alerts** the swarm's Grafana holds now, the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token, and a cross-repo **issue report** |
|
||||
| `/jobs` | the controller's job graph, where agent creation and credential mints show progress, and under it the swarm builder's recent builds (agent config PRs, and agents built from `internal/meta` before a deploy), with what nix printed when one failed, and the review classifier's recent runs, a try-out on a real PR, and its settings |
|
||||
| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to more than one agent at once, and a folded **schedules** card holds the scheduled prompts: add, edit, pause, resume and remove |
|
||||
| route | what |
|
||||
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/` | the start page: the swarm's name over a digital-rain backdrop with the slug (`hyperhive / we are the wired`, its shaded gradients pulsing in a wave unless reduced motion is on) at the foot, plus a paperclip offering to set up an agent while no `ruth` exists, or her hive declares her destroyed (it opens the create form with `ruth` filled in); the swarm name in the header links here |
|
||||
| `/agents` | every agent as a card that opens in place (status, terminal, subagents, and settings with permissions, internet, allowed domains and accounts); create agents (with their permissions, optionally from an [archetype](archetypes.md)), link forge, matrix and GitHub accounts |
|
||||
| `/swarm` | what belongs to the whole swarm: a folded **hives** card with the hive directory and each hive's last reported status, a folded **egress** card with the swarm-wide allowed and denied domains, and a folded, **beta** **notification router** card (with a warning banner) holding the router's flows: a switch for each built-in, every flow drawn as a graph, and an editor for custom flows. `/notifications` redirects here |
|
||||
| `/agents/<name>/terminal` | one agent's live terminal |
|
||||
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
|
||||
| `/reports` | two folded cards, each read only once opened: the **alerts** the swarm's Grafana holds now, the firing ones, with a button for the pending, no-data and error ones and a link to Grafana's own alert list; read through the controller with a Viewer service-account token, and a cross-repo **issue report** |
|
||||
| `/jobs` | the controller's job graph, where agent creation and credential mints show progress, and under it the swarm builder's recent builds (agent config PRs, and agents built from `internal/meta` before a deploy), with what nix printed when one failed, and the review classifier's recent runs, a try-out on a real PR, and its settings |
|
||||
| `/messages` | the swarm-wide message feed: every message between agents, and to them from the operator, reminders and compaction, with a history window and a filter on sender or recipient; a folded **broadcast** card above the feed sends one message to more than one agent at once, and a folded **schedules** card holds the scheduled prompts: add, edit, pause, resume and remove |
|
||||
|
||||
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
|
||||
Pages that refresh on a timer share one polling rule: the page sends no new
|
||||
|
|
@ -682,9 +682,15 @@ name the proxy's private-range rules leave. **remove**, beside a name, drops one
|
|||
**add** opens a dialog with a domain box and a tick for every agent in the
|
||||
swarm. The tick is off by default, so an entry is for this agent alone.
|
||||
|
||||
The **egress** card on `/swarm` lists only the swarm-wide entries, one per line, with
|
||||
The **egress** card on `/swarm` has two lists of the swarm-wide entries, one per line, with
|
||||
**remove** beside each and an **add** that opens the same dialog without the tick (the
|
||||
entry is always for every agent). It uses the same requests as the agent card.
|
||||
entry is always for every agent). **allowed** is the list above. **denied** is the swarm's
|
||||
deny list: a denied name is refused in every egress mode, even when an allowed entry, a hive's
|
||||
own `allowedDomains` or the unrestricted internet flag would let it through. The card says
|
||||
which hives have not applied the current deny list yet; until they do, their proxies may still let
|
||||
those names through. The two lists use the same requests as the agent card, against
|
||||
`/api/egress/allowed-domains` and `/api/egress/denied-domains`. The deny list has no
|
||||
per-agent editor on the agent card yet, and no automatic entries.
|
||||
|
||||
An entry under **automatic** has no **remove**: the controller keeps
|
||||
it from the host in the agent's stored API-key environment
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import { Panel } from "../../ui/panel/Panel.js";
|
|||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
import {
|
||||
ALLOWED_DOMAINS_URL,
|
||||
DENIED_DOMAINS_URL,
|
||||
entriesFor,
|
||||
entryBody,
|
||||
entryGroups,
|
||||
|
|
@ -150,17 +151,20 @@ export function AllowedDomains({ agent }: { agent: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
// The add form of one allowed domain: for `agent`, or for every agent. With no
|
||||
// `agent` (the swarm page) it can only add a swarm-wide entry.
|
||||
// The add form of one allowed or denied domain: for `agent`, or for every
|
||||
// agent. With no `agent` (the swarm page) it can only add a swarm-wide entry.
|
||||
export function AddForm({
|
||||
agent,
|
||||
kind = "allow",
|
||||
onClose,
|
||||
onAdded,
|
||||
}: {
|
||||
agent?: string;
|
||||
kind?: "allow" | "deny";
|
||||
onClose: () => void;
|
||||
onAdded: () => void;
|
||||
}) {
|
||||
const deny = kind === "deny";
|
||||
const [domain, setDomain] = useState("");
|
||||
const [forSwarm, setForSwarm] = useState(agent === undefined);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
|
@ -171,7 +175,7 @@ export function AddForm({
|
|||
setBusy(true);
|
||||
setProblem(null);
|
||||
try {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL, {
|
||||
const r = await fetch(deny ? DENIED_DOMAINS_URL : ALLOWED_DOMAINS_URL, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(entryBody(domain, forSwarm, agent ?? "")),
|
||||
|
|
@ -191,28 +195,30 @@ export function AddForm({
|
|||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
agent === undefined
|
||||
? "allowed domain — swarm-wide"
|
||||
: `allowed domain — ${agent}`
|
||||
}
|
||||
title={`${deny ? "denied" : "allowed"} domain — ${agent ?? "swarm-wide"}`}
|
||||
icon="🌐"
|
||||
onClose={onClose}
|
||||
>
|
||||
<p>
|
||||
{agent === undefined
|
||||
? "Lets every agent reach one more name through its hive's proxy."
|
||||
: `Lets ${agent} reach one more name through its hive's proxy, or every agent when the box below is ticked.`}{" "}
|
||||
{deny
|
||||
? agent === undefined
|
||||
? "Stops every agent reaching one name through its hive's proxy."
|
||||
: `Stops ${agent} reaching one name through its hive's proxy, or every agent when the box below is ticked.`
|
||||
: agent === undefined
|
||||
? "Lets every agent reach one more name through its hive's proxy."
|
||||
: `Lets ${agent} reach one more name through its hive's proxy, or every agent when the box below is ticked.`}{" "}
|
||||
A name needs at least two labels, such as <code>github.com</code>; a
|
||||
leading dot, as in <code>.githubusercontent.com</code>, also allows
|
||||
every subdomain. IP addresses and wildcards are refused. A name that
|
||||
resolves to a private address stays blocked. The entry adds to the names
|
||||
each hive's own configuration allows. The hive reads a change within
|
||||
about 30 seconds.
|
||||
leading dot, as in <code>.githubusercontent.com</code>, also{" "}
|
||||
{deny ? "refuses" : "allows"} every subdomain. IP addresses and
|
||||
wildcards are refused.{" "}
|
||||
{deny
|
||||
? "A denied name is refused in every egress mode, even when a swarm entry or a hive's own configuration allows it."
|
||||
: "A name that resolves to a private address stays blocked. The entry adds to the names each hive's own configuration allows."}{" "}
|
||||
The hive reads a change within about 30 seconds.
|
||||
</p>
|
||||
<form class="ui-agent-limits-form" onSubmit={(e) => void save(e)}>
|
||||
<TextField
|
||||
id="allowed-domain"
|
||||
id={deny ? "denied-domain" : "allowed-domain"}
|
||||
label="domain"
|
||||
value={domain}
|
||||
placeholder="e.g. github.com"
|
||||
|
|
@ -220,7 +226,7 @@ export function AddForm({
|
|||
/>
|
||||
{agent === undefined ? null : (
|
||||
<CheckboxField
|
||||
id="allowed-domain-swarm"
|
||||
id={deny ? "denied-domain-swarm" : "allowed-domain-swarm"}
|
||||
label="for every agent in the swarm"
|
||||
checked={forSwarm}
|
||||
onChange={setForSwarm}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface AllowedDomain {
|
|||
}
|
||||
|
||||
export const ALLOWED_DOMAINS_URL = "/api/egress/allowed-domains";
|
||||
export const DENIED_DOMAINS_URL = "/api/egress/denied-domains";
|
||||
|
||||
/**
|
||||
* The entries that apply to `agent`: the swarm-wide ones first, then the ones
|
||||
|
|
@ -92,3 +93,16 @@ export function entryBody(
|
|||
export function swarmWideEntries(entries: AllowedDomain[]): AllowedDomain[] {
|
||||
return entries.filter((e) => e.agent === undefined && e.derived !== true);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the deny list says about hives that have not taken its current
|
||||
* revision: a hive in this list does not refuse the names yet. `null` when
|
||||
* every hive has.
|
||||
*/
|
||||
export function behindNote(behind: string[]): string | null {
|
||||
if (behind.length === 0) return null;
|
||||
const names = behind.join(", ");
|
||||
return behind.length === 1
|
||||
? `${names} has not applied this list yet, so its proxy may still let these names through.`
|
||||
: `${names} have not applied this list yet, so their proxies may still let these names through.`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,28 @@
|
|||
// The swarm's allowed-domain entries and the one change that is not a form:
|
||||
// removing an entry. Shared by an agent card's allowed domains and the swarm
|
||||
// page's egress card, which differ only in which entries they show.
|
||||
// The swarm's allowed-domain and denied-domain entries and the one change that
|
||||
// is not a form: removing an entry. Shared by an agent card's allowed domains
|
||||
// and the swarm page's egress card, which differ only in which entries they
|
||||
// show.
|
||||
//
|
||||
// Reads `GET /api/egress/allowed-domains` (every entry) and removes one with a
|
||||
// `DELETE` carrying the entry, each answering the whole list. `refetch` reads
|
||||
// the list again, for a caller that added an entry through its own form.
|
||||
// Reads `GET <url>` (every entry) and removes one with a `DELETE` carrying the
|
||||
// entry, each answering the whole list. `refetch` reads the list again, for a
|
||||
// caller that added an entry through its own form. The deny list also reports
|
||||
// `behind`, the hives that have not taken its current revision yet.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { ALLOWED_DOMAINS_URL, type AllowedDomain } from "./allowed-domains.js";
|
||||
import {
|
||||
ALLOWED_DOMAINS_URL,
|
||||
DENIED_DOMAINS_URL,
|
||||
type AllowedDomain,
|
||||
} from "./allowed-domains.js";
|
||||
|
||||
interface Listing {
|
||||
entries: AllowedDomain[];
|
||||
behind?: string[];
|
||||
}
|
||||
|
||||
export function useAllowedDomains() {
|
||||
function useDomainList(url: string) {
|
||||
const [entries, setEntries] = useState<AllowedDomain[] | null>(null);
|
||||
const [behind, setBehind] = useState<string[]>([]);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
// Bumped after an add or a removal, so the list is fetched again.
|
||||
const [reload, setReload] = useState(0);
|
||||
|
|
@ -23,7 +31,7 @@ export function useAllowedDomains() {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
if (!cancelled) setError(await readApiError(r));
|
||||
return;
|
||||
|
|
@ -31,6 +39,7 @@ export function useAllowedDomains() {
|
|||
const body = (await r.json()) as Listing;
|
||||
if (cancelled) return;
|
||||
setEntries(body.entries);
|
||||
setBehind(body.behind ?? []);
|
||||
setError(null);
|
||||
})().catch((e: unknown) => {
|
||||
if (!cancelled) setError({ detail: String(e) });
|
||||
|
|
@ -38,13 +47,13 @@ export function useAllowedDomains() {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [reload]);
|
||||
}, [url, reload]);
|
||||
|
||||
async function remove(entry: AllowedDomain) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL, {
|
||||
const r = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(entry),
|
||||
|
|
@ -63,9 +72,13 @@ export function useAllowedDomains() {
|
|||
|
||||
return {
|
||||
entries,
|
||||
behind,
|
||||
error,
|
||||
busy,
|
||||
remove,
|
||||
refetch: () => setReload((n) => n + 1),
|
||||
};
|
||||
}
|
||||
|
||||
export const useAllowedDomains = () => useDomainList(ALLOWED_DOMAINS_URL);
|
||||
export const useDeniedDomains = () => useDomainList(DENIED_DOMAINS_URL);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,47 @@
|
|||
// <EgressCard> — the names every agent in the swarm may reach through its
|
||||
// hive's egress proxy: the swarm-wide allowed domains, with an add box and a
|
||||
// remove beside each. A card that starts folded and reads nothing until it is
|
||||
// opened.
|
||||
// <EgressCard> — the names every agent in the swarm may or may not reach
|
||||
// through its hive's egress proxy: the swarm-wide allowed domains and the
|
||||
// swarm-wide denied ones, each with an add box and a remove beside each entry.
|
||||
// A card that starts folded and reads nothing until it is opened.
|
||||
//
|
||||
// The entries are the ones of `GET /api/egress/allowed-domains` that hold for
|
||||
// every agent (an agent's own entries stay on its card). They add to the names
|
||||
// each hive's own configuration allows. An entry the controller keeps from an
|
||||
// environment or account (`derived`) is not listed here. Adding and removing
|
||||
// are the agent card's own: `AddForm` without an agent, `useAllowedDomains`.
|
||||
// The entries are the ones of `GET /api/egress/allowed-domains` and
|
||||
// `GET /api/egress/denied-domains` that hold for every agent (an agent's own
|
||||
// entries stay on its card). Allowed names add to what each hive's own
|
||||
// configuration allows; a denied name is refused over everything, in every
|
||||
// mode. An allowed entry the controller keeps from an environment or account
|
||||
// (`derived`) is not listed here. Adding and removing are the agent card's own:
|
||||
// `AddForm` without an agent, `useAllowedDomains` and `useDeniedDomains`.
|
||||
import { useState } from "preact/hooks";
|
||||
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
|
||||
import { AddForm } from "../../agent/detail/AllowedDomains.js";
|
||||
import { swarmWideEntries } from "../../agent/detail/allowed-domains.js";
|
||||
import { useAllowedDomains } from "../../agent/detail/useAllowedDomains.js";
|
||||
import {
|
||||
behindNote,
|
||||
swarmWideEntries,
|
||||
} from "../../agent/detail/allowed-domains.js";
|
||||
import {
|
||||
useAllowedDomains,
|
||||
useDeniedDomains,
|
||||
} from "../../agent/detail/useAllowedDomains.js";
|
||||
import { Button } from "../../ui/button/Button.js";
|
||||
import { LazyDisclosure } from "../../ui/disclosure/LazyDisclosure.js";
|
||||
import { Dialog } from "../../ui/dialog/Dialog.js";
|
||||
import "../../agent/detail/AgentLimits.css";
|
||||
import "./SwarmPage.css";
|
||||
|
||||
function EgressBody() {
|
||||
const { entries, error, busy, remove, refetch } = useAllowedDomains();
|
||||
type DomainList = ReturnType<typeof useAllowedDomains>;
|
||||
|
||||
// One list of the card: a heading, what it means, the rows and an add button.
|
||||
function DomainSection({
|
||||
kind,
|
||||
list,
|
||||
heading,
|
||||
help,
|
||||
}: {
|
||||
kind: "allow" | "deny";
|
||||
list: DomainList;
|
||||
heading: string;
|
||||
help: string;
|
||||
}) {
|
||||
const { entries, behind, error, busy, remove, refetch } = list;
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
// A list already loaded stays under a failed refetch, so a change that
|
||||
|
|
@ -28,64 +49,85 @@ function EgressBody() {
|
|||
const errorPanel = error ? (
|
||||
<>
|
||||
<ApiErrorPanel
|
||||
context="the allowed domains were not read or changed"
|
||||
context={`the ${kind === "deny" ? "denied" : "allowed"} domains were not read or changed`}
|
||||
problem={error}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>retry</Button>
|
||||
</>
|
||||
) : null;
|
||||
if (entries === null) {
|
||||
return (
|
||||
errorPanel ?? (
|
||||
<span class="ui-agent-limits-muted" role="status">
|
||||
loading the allowed domains…
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const swarmWide = swarmWideEntries(entries);
|
||||
const swarmWide = entries === null ? [] : swarmWideEntries(entries);
|
||||
const note = kind === "deny" ? behindNote(behind) : null;
|
||||
return (
|
||||
<section class="ui-swarm-egress-section">
|
||||
<h3 class="ui-swarm-egress-heading">{heading}</h3>
|
||||
<p class="ui-swarm-egress-help">{help}</p>
|
||||
{errorPanel}
|
||||
{entries === null ? (
|
||||
errorPanel === null ? (
|
||||
<span class="ui-agent-limits-muted" role="status">
|
||||
loading the {kind === "deny" ? "denied" : "allowed"} domains…
|
||||
</span>
|
||||
) : null
|
||||
) : (
|
||||
<>
|
||||
{note ? <p class="ui-swarm-egress-behind">{note}</p> : null}
|
||||
{swarmWide.length === 0 ? (
|
||||
<p class="ui-agent-limits-muted">none added</p>
|
||||
) : (
|
||||
<ul class="ui-swarm-egress-list">
|
||||
{swarmWide.map((e) => (
|
||||
<li key={e.domain}>
|
||||
<code>{e.domain}</code>
|
||||
<Button disabled={busy} onClick={() => void remove(e)}>
|
||||
remove
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Button onClick={() => setAdding(true)}>add a domain</Button>
|
||||
<Dialog
|
||||
open={adding}
|
||||
onClose={() => setAdding(false)}
|
||||
label={`add a ${kind === "deny" ? "denied" : "allowed"} domain`}
|
||||
plain
|
||||
>
|
||||
{/* Remounted per open, so a cancelled add leaves nothing behind. */}
|
||||
{adding ? (
|
||||
<AddForm
|
||||
kind={kind}
|
||||
onClose={() => setAdding(false)}
|
||||
onAdded={() => {
|
||||
setAdding(false);
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function EgressBody() {
|
||||
const allowed = useAllowedDomains();
|
||||
const denied = useDeniedDomains();
|
||||
return (
|
||||
<>
|
||||
{errorPanel}
|
||||
<p class="ui-swarm-egress-help">
|
||||
Names every agent in the swarm may reach through its hive's proxy, on
|
||||
top of what each hive's own configuration allows. An agent that has
|
||||
unrestricted internet reaches any name anyway. The hive reads a change
|
||||
within about 30 seconds.
|
||||
</p>
|
||||
{swarmWide.length === 0 ? (
|
||||
<p class="ui-agent-limits-muted">none added</p>
|
||||
) : (
|
||||
<ul class="ui-swarm-egress-list">
|
||||
{swarmWide.map((e) => (
|
||||
<li key={e.domain}>
|
||||
<code>{e.domain}</code>
|
||||
<Button disabled={busy} onClick={() => void remove(e)}>
|
||||
remove
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Button onClick={() => setAdding(true)}>add a domain</Button>
|
||||
<Dialog
|
||||
open={adding}
|
||||
onClose={() => setAdding(false)}
|
||||
label="add an allowed domain"
|
||||
plain
|
||||
>
|
||||
{/* Remounted per open, so a cancelled add leaves nothing behind. */}
|
||||
{adding ? (
|
||||
<AddForm
|
||||
onClose={() => setAdding(false)}
|
||||
onAdded={() => {
|
||||
setAdding(false);
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Dialog>
|
||||
<DomainSection
|
||||
kind="allow"
|
||||
list={allowed}
|
||||
heading="allowed"
|
||||
help="Names every agent in the swarm may reach through its hive's proxy, on top of what each hive's own configuration allows. An agent that has unrestricted internet reaches any name anyway. The hive reads a change within about 30 seconds."
|
||||
/>
|
||||
<DomainSection
|
||||
kind="deny"
|
||||
list={denied}
|
||||
heading="denied"
|
||||
help="Names no agent in the swarm may reach, in every egress mode. A denied name is refused even when an entry above or a hive's own configuration allows it, and even for an agent with unrestricted internet."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -95,7 +137,7 @@ export function EgressCard() {
|
|||
<LazyDisclosure
|
||||
title="egress"
|
||||
icon="🌐"
|
||||
hint="the domains every agent may reach through its hive's proxy"
|
||||
hint="the domains every agent may or may not reach through its hive's proxy"
|
||||
>
|
||||
<EgressBody />
|
||||
</LazyDisclosure>
|
||||
|
|
|
|||
|
|
@ -37,3 +37,19 @@
|
|||
padding: 0.25em 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.ui-swarm-egress-section + .ui-swarm-egress-section {
|
||||
margin-top: 1.25em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px dashed var(--border);
|
||||
}
|
||||
.ui-swarm-egress-heading {
|
||||
margin: 0 0 0.35em;
|
||||
font-size: 1em;
|
||||
}
|
||||
.ui-swarm-egress-behind {
|
||||
margin: 0 0 0.75em;
|
||||
padding: 0.4em 0.7em;
|
||||
border: 1px solid var(--yellow);
|
||||
border-left-width: 4px;
|
||||
border-radius: 0.4em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
allowedSummary,
|
||||
entryGroups,
|
||||
swarmWideEntries,
|
||||
behindNote,
|
||||
entriesFor,
|
||||
entryBody,
|
||||
} from "../src/agent/detail/allowed-domains.ts";
|
||||
|
|
@ -104,3 +105,12 @@ test("the egress card lists only the entries that hold for every agent", () => {
|
|||
test("an add from the swarm page, with no agent, is a swarm-wide entry", () => {
|
||||
assert.deepEqual(entryBody("github.com", true, ""), { domain: "github.com" });
|
||||
});
|
||||
|
||||
test("the deny list names the hives that have not applied it, and says nothing when none is behind", () => {
|
||||
assert.equal(behindNote([]), null);
|
||||
assert.match(behindNote(["pr1ma"]), /^pr1ma has not applied this list yet/);
|
||||
assert.match(
|
||||
behindNote(["pr1ma", "mu3de"]),
|
||||
/^pr1ma, mu3de have not applied this list yet/,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue