Watch
0
0
Fork
You've already forked hyperhive
0

setup assistant creates ruth with logs and metrics; docs for the harness gate, ordering and pass

This commit is contained in:
atlas 2026-11-01 18:27:55 +01:00
commit 8a60f52afa
5 changed files with 44 additions and 22 deletions

View file

@ -316,19 +316,30 @@ none. swarm-controller logs every refused `PUT` or `DELETE` on that route at `wa
**`logs` and `metrics`** — off for every agent until granted, and separate
because logs are the more sensitive. Grant one with
`swarmctl agent permissions set <agent>` (listing it with the others) or the
create-agent screen. An agent without it gets a 403 from the store's gateway
when `swarm-logs` or `swarm-metrics` queries; a bad secret is still a 401. The
controller refuses the grant, with a 409 naming the hive, unless the agent's
hive advertises `tolerant-permissions` in its status snapshot
(`swarm_wire::hive_status`): a hive that predates the permission would fail to
decode the record. It also mirrors the two grants to the store leaf
create-agent screen; the setup assistant creates ruth with both. An agent
without it gets a 403 from the store's gateway when `swarm-logs` or
`swarm-metrics` queries; a bad secret is still a 401.
An agent decodes its own record, in its harness
(`hive-agent/src/swarm_permissions.rs`, through `swarm_queue_client::agent_permissions::read`),
and a build that doesn't know a name would drop to no permissions. So
swarm-controller refuses a change that adds `logs` or `metrics` to an existing
agent, with a 409, unless both the agent's hive (`tolerant-permissions` in its
status snapshot) and its running harness (the same entry in its turn-state
header, `swarm_wire::hive_status`) advertise it. A harness not yet updated, a
stopped agent and one that hasn't sent its first header (up to a minute after
a restart) all refuse; keeping a grant it already holds needs no report. A
create has no harness yet and checks the hive only.
The controller mirrors the two grants to the store leaf
`swarm/agents/<agent>/observability` (`swarm_secret_client::observability`),
which the gateways read; the gateway denies when it can't read that leaf. A
revoked grant stops working within `authVerdictTtl` (60 seconds by default) of
the agent's last allowed query. Grant it only after the agent runs a harness
that skips names it doesn't know (its next update after the release that adds
these two); an older harness that reads such a record holds no permissions at
its next start.
which the gateways read, and the gateway denies when it can't read that leaf.
A change that drops a grant writes the leaf first and the record second, so a
failed write changes nothing; a change that adds one writes the record first,
so a failed write leaves the gateway denying. Every five minutes the controller
also makes each agent's leaf equal its record and logs a warning for each leaf
it corrects. A revoked grant stops working within `authVerdictTtl` (60
seconds by default) of the agent's last allowed query.
**Internet flag** — the record also carries `unrestricted_internet`, a
boolean that isn't a `Permission`. An absent value means on, so every agent

View file

@ -29,7 +29,7 @@ the rest of the cell says how.
| `swarm/controller/swarm-controller/forge/webhook-secret` | `swarm-controller`, at start, from its old `webhook-secret` file; a new one only when there is no file | `swarm-controller` | ❌ minted once; a rotated secret fails every delivery until the forge hooks are re-registered | ❌ read at start |
| `swarm/agents/<agent>/bao-mtls` | the store's agent PKI mount (`deploy.bao.agentPkiMountPath`), at agent creation, and by the renewal pass when none is stored or its cert-auth role is missing | `hive-c0re`, then the agent | ✅ re-issued every 5 minutes once past half its 7-day validity; minted when none is stored or its cert-auth role is missing, for agents not `Destroyed` | ✅ the agent re-reads every 10 minutes; it renews only while its current leaf is valid |
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | the agent; the queue, log and metrics auth services | ✅ re-minted at 45 days; skipped for `Destroyed` agents, whose path is deleted | ✅ read on every reconnect; an open connection keeps the old secret for up to an hour; the log and metrics stores honour a replaced secret for up to `deploy.victorialogs.authVerdictTtl` and `deploy.victoriametrics.agentAuthVerdictTtl` (60 s by default) |
| `swarm/agents/<agent>/observability` | `swarm-controller`, when an operator sets, clears or creates an agent's permissions with `logs` or `metrics` in the old or new list, and when it destroys the agent; the leaf stays absent for an agent that never held either | the agent (read-only, its own prefix); `swarm-logs-auth` and `swarm-metrics-agent-auth`, which deny when it's absent or unreadable | ❌ the controller rewrites it with the permission; nothing refreshes it | ✅ the stores honour a changed grant for up to `deploy.victorialogs.authVerdictTtl` and `deploy.victoriametrics.agentAuthVerdictTtl` (60 s by default) |
| `swarm/agents/<agent>/observability` | `swarm-controller`, when an operator sets, clears or creates an agent's permissions with `logs` or `metrics` in the old or new list, and when it destroys the agent; the leaf stays absent for an agent that never held either | the agent (read-only, its own prefix); `swarm-logs-auth` and `swarm-metrics-agent-auth`, which deny when it's absent or unreadable | ✅ the controller rewrites it with the permission, and every five minutes makes it equal the record's | ✅ the stores honour a changed grant for up to `deploy.victorialogs.authVerdictTtl` and `deploy.victoriametrics.agentAuthVerdictTtl` (60 s by default) |
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and every 5 minutes | the agent | ✅ when missing or no longer matching the forge | ✅ every 10 minutes |
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when you link an external forge account in the swarm UI | the agent | ❌ your token; delete it and link the label again | ✅ every 2 minutes |
| `swarm/agents/<agent>/github-token` | `swarm-controller`, when you link a GitHub account in the swarm UI | the agent | ❌ your token; delete it and link again | ✅ every 2 minutes |

View file

@ -1,8 +1,8 @@
// <SetupAssistant> - the start page's first-time helper: a paperclip with a
// speech bubble offering to set up an agent. It renders only while the swarm
// has no `ruth` (see `ruthIsMissing`), and a click opens the create-agent
// form with her name filled in and `manage_root_agent` checked on top of the
// default permissions (`RUTH_EXTRA_PERMISSIONS`); the form already selects
// form with her name filled in and `manage_root_agent`, `logs` and
// `metrics` checked on top of the default permissions (`RUTH_EXTRA_PERMISSIONS`); the form already selects
// the only hive. No dismiss: once ruth exists it goes away.
import { useEffect, useState } from "preact/hooks";
import { Dialog } from "../ui/dialog/Dialog.js";

View file

@ -27,5 +27,9 @@ export async function ruthIsMissing(fetchImpl: Fetch): Promise<boolean> {
}
/** What the setup assistant's create form checks on top of the default set:
* ruth recovers the other agents, which takes `manage_root_agent`. */
export const RUTH_EXTRA_PERMISSIONS = ["manage_root_agent"];
* ruth recovers the other agents, which takes `manage_root_agent`, and reads
* the swarm's logs and metrics, which are off by default. swarm-controller
* refuses `logs` and `metrics` (409, naming the hive) for a hive that does
* not advertise `tolerant-permissions`; the form shows that error, and
* unticking them creates ruth without. */
export const RUTH_EXTRA_PERMISSIONS = ["manage_root_agent", "logs", "metrics"];

View file

@ -58,17 +58,24 @@ test("anything but a clear answer is not missing", async () => {
);
});
// The assistant's create form sends `manage_root_agent` with the default
// set, so a new ruth holds it from her first start; the default alone does not.
test("the assistant creates ruth with manage_root_agent", () => {
// The assistant's create form sends `manage_root_agent`, `logs` and `metrics` with
// the default set, so a new ruth holds them from her first start; the default
// alone does not.
test("the assistant creates ruth with manage_root_agent, logs and metrics", () => {
const defaults = ["messaging", "meta", "inbox", "execution"];
const all = [...defaults, "web_tools", "manage_root_agent"];
const all = [
...defaults,
"web_tools",
"manage_root_agent",
"logs",
"metrics",
];
assert.deepEqual(
requestPermissions(
initialSelection(all, defaults, RUTH_EXTRA_PERMISSIONS),
defaults,
),
[...defaults, "manage_root_agent"],
[...defaults, "manage_root_agent", "logs", "metrics"],
);
assert.equal(
requestPermissions(initialSelection(all, defaults, []), defaults),