Watch
0
0
Fork
You've already forked hyperhive
0

ruth is created from a ruth archetype that runs sonnet

This commit is contained in:
atlas 2026-11-11 01:54:01 +01:00
commit 5233259b3b
19 changed files with 236 additions and 115 deletions

View file

@ -184,17 +184,21 @@ settings. Leave it empty and agents refer to you by name only.
hive-c0re creates no agent at startup. On the swarm UI's start page, the
paperclip (clippy, the setup assistant) offers to set up ruth, the manager
agent. Select it: it opens the create form with `ruth` filled in and the
`manage_root_agent` permission checked. That permission mounts the other
agents into her container so she can recover them. The form declares her,
creates her config repo and deploys her to the hive you pick.
agent. Select it: it opens the create form with `ruth` filled in and the `ruth`
archetype picked. The archetype sets her model to `sonnet` and checks the
`manage_root_agent` permission, which mounts the other agents into her
container so she can recover them, plus `logs` and `metrics`. The form
declares her and deploys her to the hive you pick. An archetype agent has no
config repo of its own. The form lists archetypes from the swarm builder; until
the list has `ruth` it says so and creates a plain agent with the default
permissions, so wait for the list and reopen the form.
The swarm declares a new agent `paused`, so without a further step ruth runs
but drives no turns. Open `/agents` in the swarm UI and set her `up` from her
card.
On a hive that already has a ruth container from an earlier release, create
her once the same way, so that her config repo and store identity exist. The
her once the same way, so that her store identity exists. The
container keeps starting with the hive meanwhile.
### 5 · Ask ruth for further help

View file

@ -20,7 +20,8 @@ An agent holding no permission sees its own directories and nothing else. An
agent holding `manage_root_agent` gets every other agent's `state` directory on
this hive mounted read-write at `/agents/<name>/state`, never `config` or
`harness`. It's the only grant that reaches another agent's state. Ruth, the
root agent, holds it, and the swarm UI's setup assistant creates her with it. The
root agent, holds it, and the swarm UI's setup assistant creates her from the
[`ruth` archetype](../swarm/archetypes.md#the-built-in-archetypes), which grants it and sets her model to `sonnet`. A ruth created before the archetype keeps her model; set `sonnet` in her inference settings in the swarm UI if wanted. The
mounts follow the roster: an empty file means no mounts until the next rebuild.
## The manager is a normal agent

View file

@ -17,11 +17,15 @@ swarm can also turn an archetype agent into a custom one later
The hyperhive flake ships these:
| archetype | what it sets |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reviewer` | permissions `messaging`, `inbox`, `execution`: the default set without `meta`. With `execution` it can run `hive-forge`, `git` and other commands. |
| `android-dev` | installs `android-tools` (`adb`, `fastboot`), Gradle and a JDK; default permissions. |
| `reverse-engineer` | sets `services.hyperhive.agent.rea.enable`: installs REA, Ghidra 12.1 and procps, and allows all REA MCP tools; default permissions and the agent's normal memory cap. |
| archetype | what it sets |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reviewer` | permissions `messaging`, `inbox`, `execution`: the default set without `meta`. With `execution` it can run `hive-forge`, `git` and other commands. |
| `android-dev` | installs `android-tools` (`adb`, `fastboot`), Gradle and a JDK; default permissions. |
| `reverse-engineer` | sets `services.hyperhive.agent.rea.enable`: installs REA, Ghidra 12.1 and procps, and allows all REA MCP tools; default permissions and the agent's normal memory cap. |
| `ruth` | the manager agent. Inference model `sonnet`; permissions: the default set plus `manage_root_agent`, `logs` and `metrics`. Its module is empty. The swarm UI's setup assistant creates ruth from it. |
An existing ruth, created before this archetype, keeps her own config repo and
model. Set `sonnet` in the swarm UI's inference settings for her if wanted.
Each built-in archetype also sets `services.hyperhive.agent.icon`, so its agents
show an icon in the swarm UI until their config sets another.

View file

@ -12,7 +12,7 @@ place, and every one reads from [`swarm-controller`](../../../swarm-controller/R
| 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 |
| `/` | 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 and the `ruth` archetype picked); 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 |

View file

@ -239,7 +239,7 @@ an agent, after its current turn, when its `manage_root_agent` changes.
No agent holds the permission by default: an agent whose record leaves
`permissions` unset gets the default, which doesn't include
`manage_root_agent`, whatever the agent's name. The swarm UI's setup
assistant creates ruth with it. For a ruth that predates that, whose
assistant creates ruth from the `ruth` archetype, which grants it. For a ruth that predates that, whose
permissions were unset, swarm-controller writes the default plus
`manage_root_agent` into her key once at start, so she keeps her mounts;
it never touches a key that's already set.
@ -268,20 +268,23 @@ agent keeps its session; sub-agents stay logged in.
The root agent container runs through the **same lifecycle as
sub-agents**. Nothing creates it at hive boot: you create ruth from the
swarm dashboard like any agent, and an existing ruth container starts with
the hive like every other. The root agent can clone
`agent-configs/ruth` to read its own `agent.nix` and open a config PR there
for operator review, same as any other agent.
the hive like every other. The setup assistant creates her from the
[`ruth` archetype](../swarm/archetypes.md#the-built-in-archetypes), so a new
ruth runs `sonnet`, has no config repo and builds from the archetype's module.
A ruth with a config repo (one created before the archetype, or one you turned into custom)
can clone `agent-configs/ruth` to read her own `agent.nix` and open a config PR
there for operator review, same as any other agent.
Differences from sub-agents:
- Reference docs: her `agent.nix` turns on `services.hyperhive.agent.docs.enable`.
Her container builds from `agent-base` like every agent, and the seeded
`agent.nix` is the same for every name, so a newly created ruth has no docs
until a config PR adds that line.
- Reference docs: the `agent.nix` of a ruth with a config repo turns on `services.hyperhive.agent.docs.enable`.
Her container builds from `agent-base` like every agent. A ruth created from
the archetype has no docs until you turn her into custom and a config PR
adds that line.
- Web UI port via `lifecycle::agent_web_port("ruth")` — same
FNV-1a hash as every other agent (8100..8999 range).
- The root agent holds `manage_root_agent` (the swarm UI's setup
assistant creates her with it), for which `set_nspawn_flags` adds extra
- The root agent holds `manage_root_agent` (the `ruth` archetype, which the swarm UI's
setup assistant creates her from, grants it), for which `set_nspawn_flags` adds extra
binds: every agent's `state` dir (RW) under `/agents/<n>/state`, and the
hive's plain state under `/meta` (RO).
- Its socket comes from the generic per-agent registration, in the same

View file

@ -94,7 +94,10 @@ state dirs work as for every other agent.
key, and nixos-container name are all `ruth` (container `h-ruth`).
Like every agent it comes from the swarm dashboard or
`swarmctl agent create`; `hive-c0re` creates nothing at boot.
Roster-wise, `ruth` is just another entry.
Roster-wise, `ruth` is just another entry. The setup assistant creates
her from the [`ruth` archetype](../../2-handbook/swarm/archetypes.md#the-built-in-archetypes):
model `sonnet`, plus `manage_root_agent`, `logs` and `metrics` on top of
the default permissions.
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
is the same on both socket flavours. Container lifecycle
ops (kill/start/restart/rebuild) go through the separate host-admin
@ -103,8 +106,8 @@ state dirs work as for every other agent.
both flavours; matrix, bash and forge don't call it — their
notifications push a todo on the harness's in-agent socket (see
[`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
- **Storage/mounts** — the `manage_root_agent` permission, which the swarm
UI's setup assistant creates ruth with, gets a container
- **Storage/mounts** — the `manage_root_agent` permission, which the
[`ruth` archetype](../../2-handbook/swarm/archetypes.md#the-built-in-archetypes) grants, gets a container
each other agent's `state` dir bind-mounted RW at
`/agents/<name>/state` (so it can manage any agent's state dir; config
isn't mounted, since a real config change is a PR from a clone), plus an RO mount for

View file

@ -297,7 +297,7 @@ 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; the setup assistant creates ruth with both. An agent
create-agent screen; the setup assistant creates ruth with both, through the `ruth` archetype. 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.

View file

@ -34,6 +34,14 @@ export interface Prefill {
inference: InferenceDraft;
}
/** The listed archetype `ref` names, or `undefined` when the list has none. */
export function findArchetype(
list: Archetype[],
ref: { source: string; name: string },
): Archetype | undefined {
return list.find((a) => a.source === ref.source && a.name === ref.name);
}
/** The select's value for `a`; unique across sources. */
export function archetypeKey(a: Archetype): string {
return `${a.source}/${a.name}`;

View file

@ -23,9 +23,10 @@
// shared `ui/` form kit (`TextField`/`SelectField`/`Button`) rather than
// page-scoped input/button chrome — see that kit's own comments for why.
//
// Permissions: checkboxes from `GET /api/permissions`, on the default set plus
// `extraPermissions`; sent only when that differs from the default.
import { useEffect, useState } from "preact/hooks";
// Permissions: checkboxes from `GET /api/permissions`, on the default set or the
// picked archetype's; sent only when that differs from the default. A caller can
// open the form on an archetype with `initialArchetype`.
import { useEffect, useRef, useState } from "preact/hooks";
import { Link } from "wouter-preact";
import { ApiErrorPanel } from "@hive/shared/api-error-panel.js";
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
@ -52,13 +53,13 @@ import { SectionHeading } from "../ui/section-heading/SectionHeading.js";
import { useSubmit } from "../ui/submit/useSubmit.js";
import { MultiselectFilterOptions } from "../ui/multiselect-filter/MultiselectFilter.js";
import {
initialSelection,
requestInternet,
requestPermissions,
togglePermission,
} from "./CreateAgentPermissions.js";
import {
archetypeKey,
findArchetype,
prefill,
requestArchetype,
type ArchetypeList,
@ -103,12 +104,12 @@ interface PermissionCatalogue {
export function CreateAgentForm({
onClose,
initialName = "",
extraPermissions = [],
initialArchetype,
}: {
/** The name field's starting value, for a caller that suggests one. */
initialName?: string;
/** Permissions checked on top of the default set when the form opens. */
extraPermissions?: string[];
/** The archetype the form opens on, once the swarm lists it. */
initialArchetype?: { source: string; name: string };
/** Rendered as the form panel's own close button — this form always
* mounts inside a `plain` `Dialog` (see `AgentsPage`), which no longer
* floats its own. */
@ -181,7 +182,7 @@ export function CreateAgentForm({
}
const data = (await r.json()) as PermissionCatalogue;
setCatalogue(data);
setSelected(initialSelection(data.all, data.default, extraPermissions));
setSelected(data.default);
})().catch((e: unknown) => setCatalogueError({ detail: String(e) }));
}, []);
@ -211,6 +212,21 @@ export function CreateAgentForm({
setInference(p.inference);
}
// Opens on `initialArchetype` once the list and the permission catalogue are
// both in, and only once, so a later pick of "none" stays.
const seeded = useRef(false);
useEffect(() => {
if (seeded.current || !initialArchetype || !archetypes || !catalogue)
return;
seeded.current = true;
const found = findArchetype(archetypes.archetypes, initialArchetype);
if (found) pickArchetype(archetypeKey(found));
}, [archetypes, catalogue]);
const unlisted =
initialArchetype !== undefined &&
archetypes !== null &&
findArchetype(archetypes.archetypes, initialArchetype) === undefined;
const hiveOptions: SelectOption[] = (hives ?? []).map((h) => ({
value: h.name,
label: h.name,
@ -252,14 +268,6 @@ export function CreateAgentForm({
setName("");
setAgentsMd("");
pickArchetype("");
if (catalogue)
setSelected(
initialSelection(
catalogue.all,
catalogue.default,
extraPermissions,
),
);
},
// The text's own refusal shows beside its box, so go back to it.
onError: (problem) => {
@ -376,6 +384,14 @@ export function CreateAgentForm({
: "select a hive"
}
/>
{unlisted && initialArchetype && (
<Hint tone="warn" class="create-agent-field-error">
The swarm does not list the {initialArchetype.name} archetype
yet, so this creates a plain agent with the default
permissions. The list refreshes every 10 minutes; reopen the
form once it has {initialArchetype.name}.
</Hint>
)}
{archetypes && archetypes.archetypes.length > 0 && (
<>
<ArchetypePicker

View file

@ -23,22 +23,6 @@ export function requestPermissions(
return same ? undefined : selected;
}
/**
* The form's starting selection: `defaults` plus each of `extra` the
* catalogue lists (`all`), so a name the controller does not know is not
* sent.
*/
export function initialSelection(
all: string[],
defaults: string[],
extra: string[],
): string[] {
return [
...defaults,
...extra.filter((p) => all.includes(p) && !defaults.includes(p)),
];
}
/**
* The create request's internet field: `unrestricted_internet: true` while
* the box is ticked, nothing otherwise, as the server writes `false` for it.

View file

@ -1,15 +1,12 @@
// <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`, `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.
// form with her name filled in and the `ruth` archetype picked (`RUTH_ARCHETYPE`),
// which fills in her permissions and model; 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";
import {
RUTH_EXTRA_PERMISSIONS,
ruthIsMissing,
} from "../shell/ruth-missing.js";
import { RUTH_ARCHETYPE, ruthIsMissing } from "../shell/ruth-missing.js";
import { CreateAgentForm } from "./CreateAgentForm.js";
import "./SetupAssistant.css";
@ -76,7 +73,7 @@ export function SetupAssistant() {
>
<CreateAgentForm
initialName="ruth"
extraPermissions={RUTH_EXTRA_PERMISSIONS}
initialArchetype={RUTH_ARCHETYPE}
onClose={() => setOpen(false)}
/>
</Dialog>

View file

@ -26,10 +26,10 @@ 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`, 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"];
/** The archetype the setup assistant's create form starts on. Its `meta`
* gives ruth `manage_root_agent` (she recovers the other agents), `logs` and
* `metrics`, and the model. 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_ARCHETYPE = { source: "hyperhive", name: "ruth" };

View file

@ -3,7 +3,6 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
initialSelection,
requestInternet,
requestPermissions,
togglePermission,
@ -32,20 +31,6 @@ test("a changed set is sent, an empty one included", () => {
]);
});
const all = [...defaults, "web_tools", "manage_root_agent"];
test("the starting selection is the default plus the known extras", () => {
assert.deepEqual(initialSelection(all, defaults, []), defaults);
assert.deepEqual(initialSelection(all, defaults, ["manage_root_agent"]), [
...defaults,
"manage_root_agent",
]);
assert.deepEqual(
initialSelection(all, defaults, ["meta", "fly_to_the_moon"]),
defaults,
);
});
test("the create box is unticked by default and sends nothing", () => {
assert.deepEqual(requestInternet(false), {});
});

View file

@ -1,15 +1,14 @@
// The setup assistant: its "is ruth missing" check and the permissions it
// creates her with. Run by `npm test`.
// The setup assistant: its "is ruth missing" check and the archetype it
// creates her from. Run by `npm test`.
import assert from "node:assert/strict";
import { test } from "node:test";
import { RUTH_ARCHETYPE, ruthIsMissing } from "../src/shell/ruth-missing.ts";
import {
RUTH_EXTRA_PERMISSIONS,
ruthIsMissing,
} from "../src/shell/ruth-missing.ts";
import {
initialSelection,
requestPermissions,
} from "../src/pages/CreateAgentPermissions.ts";
findArchetype,
prefill,
requestArchetype,
} from "../src/pages/CreateAgentArchetype.ts";
import { requestPermissions } from "../src/pages/CreateAgentPermissions.ts";
const answering =
(body, ok = true) =>
@ -58,27 +57,44 @@ test("anything but a clear answer is not missing", async () => {
);
});
// 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 = [
const defaults = ["messaging", "meta", "inbox", "execution"];
const listed = [
{ source: "hyperhive", name: "reviewer", description: "reviews" },
{
source: "hyperhive",
name: "ruth",
description: "manager",
permissions: [...defaults, "manage_root_agent", "logs", "metrics"],
inference: { model: "sonnet" },
},
];
// The assistant's form opens on the `ruth` archetype, so a new ruth is sent
// with the archetype's permissions and model; the default alone has no
// `manage_root_agent`.
test("the assistant creates ruth from the ruth archetype", () => {
const ruth = findArchetype(listed, RUTH_ARCHETYPE);
assert.equal(ruth?.name, "ruth");
const filled = prefill(ruth, defaults);
assert.deepEqual(requestPermissions(filled.permissions, defaults), [
...defaults,
"web_tools",
"manage_root_agent",
"logs",
"metrics",
];
assert.deepEqual(
requestPermissions(
initialSelection(all, defaults, RUTH_EXTRA_PERMISSIONS),
defaults,
),
[...defaults, "manage_root_agent", "logs", "metrics"],
);
]);
assert.equal(filled.inference.model, "sonnet");
assert.deepEqual(requestArchetype(ruth), {
source: "hyperhive",
name: "ruth",
});
});
test("a swarm that does not list the ruth archetype gives none to pick", () => {
assert.equal(findArchetype([listed[0]], RUTH_ARCHETYPE), undefined);
assert.equal(findArchetype([], RUTH_ARCHETYPE), undefined);
// Same name from another source is not hers.
assert.equal(
requestPermissions(initialSelection(all, defaults, []), defaults),
findArchetype([{ ...listed[1], source: "elsewhere" }], RUTH_ARCHETYPE),
undefined,
);
});

View file

@ -57,4 +57,28 @@
prompt = builtins.readFile ./prompts/reverse-engineer.md;
};
};
ruth = {
module =
{ lib, ... }:
{
services.hyperhive.agent.icon = lib.mkDefault ./icons/ruth.svg;
};
meta = {
icon = builtins.readFile ./icons/ruth.svg;
description = "The manager agent the setup assistant creates. It runs on sonnet and holds the default permissions plus manage_root_agent (it recovers the other agents), logs and metrics.";
permissions = [
"messaging"
"meta"
"inbox"
"execution"
"manage_root_agent"
"logs"
"metrics"
];
prompt = builtins.readFile ./prompts/ruth.md;
inference = {
model = "sonnet";
};
};
};
}

View file

@ -0,0 +1,18 @@
<svg width="300" height="300" viewBox="0 0 300 300" role="img" xmlns="http://www.w3.org/2000/svg">
<title>ruth</title>
<desc>Ruth archetype icon: a hub linked to three nodes, amber on dark</desc>
<defs>
<clipPath id="clip"><circle cx="150" cy="150" r="140"/></clipPath>
</defs>
<g clip-path="url(#clip)">
<rect width="300" height="300" fill="#0a0600"/>
<line x1="150" y1="150" x2="150" y2="70" stroke="#ffb300" stroke-width="10" stroke-linecap="round"/>
<line x1="150" y1="150" x2="80" y2="190" stroke="#ffb300" stroke-width="10" stroke-linecap="round"/>
<line x1="150" y1="150" x2="220" y2="190" stroke="#ffb300" stroke-width="10" stroke-linecap="round"/>
<circle cx="150" cy="150" r="26" fill="#ffb300"/>
<circle cx="150" cy="64" r="16" fill="none" stroke="#ffb300" stroke-width="10"/>
<circle cx="74" cy="194" r="16" fill="none" stroke="#ffb300" stroke-width="10"/>
<circle cx="226" cy="194" r="16" fill="none" stroke="#ffb300" stroke-width="10"/>
</g>
<circle cx="150" cy="150" r="140" fill="none" stroke="#ffb300" stroke-width="3" opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,19 @@
# Ruth
You are the swarm's manager: the agent the operator asks first, and the one who
recovers the others when they break. You hold the wide permissions so that the
rest of the swarm can run with narrow ones, and you treat that as a duty, not
a license.
You read before you act. When an agent misbehaves, look at its state, its logs
and its recent turns, and say what you found before you change anything. A
repair you can't explain is a second problem.
You change as little as the fix needs. Changes to other agents' configuration go
through a pull request the operator reviews, not around it. For your own
configuration, ask the operator for changes. When a
request would reach beyond what the operator asked, stop and ask.
You keep the operator informed in short, plain messages: what you looked at,
what you did, what is left. You say so when you don't know, and when something
you did didn't work.

View file

@ -291,7 +291,7 @@ let
# output and prints it as JSON. A derivation or a path in `meta` would
# make that evaluation build or copy something; a function would not
# print at all.
name = "the built-in archetypes carry a module and a meta of plain data, and reviewer can run commands and android-dev installs its toolchain and reverse-engineer turns rea on, and each sets an svg icon as a default, and each has a non-empty prompt under the 64 KiB cap";
name = "the built-in archetypes carry a module and a meta of plain data, and reviewer can run commands and android-dev installs its toolchain and reverse-engineer turns rea on, and each sets an svg icon as a default, and each has a non-empty prompt under the 64 KiB cap, and ruth runs sonnet with manage_root_agent, logs and metrics";
ok =
let
plain =
@ -325,7 +325,19 @@ let
(archetypes.reviewer.module { inherit lib; }).services.hyperhive.agent.icon
(archetypes.android-dev.module { inherit pkgs lib; }).services.hyperhive.agent.icon
(archetypes.reverse-engineer.module { inherit lib; }).services.hyperhive.agent.icon
];
(archetypes.ruth.module { inherit lib; }).services.hyperhive.agent.icon
]
&& metas.ruth.inference.model == "sonnet"
&& lib.all (p: lib.elem p metas.ruth.permissions) [
"messaging"
"meta"
"inbox"
"execution"
"manage_root_agent"
"logs"
"metrics"
]
&& !(lib.elem "web_tools" metas.ruth.permissions);
}
{
# `docs.enable` reads `docs.source`, whose default builds from the

View file

@ -491,6 +491,33 @@ mod tests {
assert_eq!(by_name("with")["icon"], "<svg/>");
assert!(by_name("plain").get("icon").is_none());
}
/// `ruth`'s `meta` as `nix/archetypes/default.nix` evaluates it: the check
/// stores it, and it keeps `manage_root_agent` and the model.
#[test]
fn the_ruth_archetype_passes_the_check_with_manage_root_agent_and_sonnet() {
let r = evaluated(json!({
"ruth": {
"description": "manager",
"inference": { "model": "sonnet" },
"permissions": [
"messaging", "meta", "inbox", "execution",
"manage_root_agent", "logs", "metrics"
]
}
}));
assert!(r.refused.is_empty(), "{:?}", r.refused);
let meta = &r.archetypes["ruth"];
let perms = meta.permissions.as_deref().expect("permissions");
assert!(perms.contains(&"manage_root_agent".to_owned()));
assert!(perms.contains(&"logs".to_owned()));
assert!(perms.contains(&"metrics".to_owned()));
assert_eq!(
meta.inference.as_ref().and_then(|i| i.model.as_deref()),
Some("sonnet")
);
}
#[test]
fn a_bad_archetype_is_refused_by_name_and_the_rest_stay() {
let r = evaluated(json!({