refactor(hive-c0re): drop the request_init_config tool and InitConfig approval
swarm-controller's `InitAgentConfigRepo` node already covers config-repo creation, so this deletes a duplicate rather than a capability; old `init_config` rows are skipped by `collect_lenient` with no migration, by operator decision. Refs #4398
This commit is contained in:
parent
3f086bc659
commit
a3b672d1d5
31 changed files with 134 additions and 601 deletions
|
|
@ -26,7 +26,7 @@ declarations.
|
||||||
## Agent lifecycle
|
## Agent lifecycle
|
||||||
|
|
||||||
- **How do config changes flow from manager to operator to container?** →
|
- **How do config changes flow from manager to operator to container?** →
|
||||||
[`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (two-step spawn, approval
|
[`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (approval kinds, approval
|
||||||
state machine, `flake.lock` validation).
|
state machine, `flake.lock` validation).
|
||||||
- **What state survives destroy / purge / restart?** →
|
- **What state survives destroy / purge / restart?** →
|
||||||
[`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md).
|
[`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md).
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,10 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside
|
||||||
|
|
||||||
`null` = root-level agent. New agents **default to root** — there is no
|
`null` = root-level agent. New agents **default to root** — there is no
|
||||||
structural manager that everything hangs under. Hierarchy is built
|
structural manager that everything hangs under. Hierarchy is built
|
||||||
explicitly: an agent that requests a sub-agent gets a
|
explicitly: an agent gets a parent edge written before its first spawn,
|
||||||
requester-as-parent edge written at its `init_config` approval (so
|
or the operator reparents it afterwards (so `bob` above sits under
|
||||||
`alice` spawned `bob` above), and the operator can reparent any
|
`alice`). Any agent is reparentable, the bootstrap container (`ruth`)
|
||||||
agent, including the bootstrap container (`ruth`) — it's just another
|
included — it's just another root. The manager is reparentable like any other agent; there's no
|
||||||
root. The manager is reparentable like any other agent; there's no
|
|
||||||
"structurally root" carve-out. Its privileges live on its MCP socket,
|
"structurally root" carve-out. Its privileges live on its MCP socket,
|
||||||
not its tree position (see _Manager special-casing today_ below).
|
not its tree position (see _Manager special-casing today_ below).
|
||||||
|
|
||||||
|
|
@ -63,10 +62,11 @@ where system-level facts live.
|
||||||
install that hasn't synced yet).
|
install that hasn't synced yet).
|
||||||
- **Reconcile** — runs alongside the periodic meta/flake regeneration.
|
- **Reconcile** — runs alongside the periodic meta/flake regeneration.
|
||||||
New agents default to root unless they already carry an explicit
|
New agents default to root unless they already carry an explicit
|
||||||
parent edge from an `init_config` approval; Reconcile preserves existing entries
|
parent edge written before their first spawn; Reconcile preserves
|
||||||
(including operator overrides); removed agents drop.
|
existing entries (including operator overrides); removed agents drop.
|
||||||
Agents that are approved but not yet spawned keep their edge too, so
|
Agents whose config repo exists but that haven't spawned yet keep
|
||||||
it survives the gap until the container actually appears.
|
their edge too, so it survives the gap until the container actually
|
||||||
|
appears.
|
||||||
- **Inject** — hive-c0re exposes each container's parent (if any) to its own
|
- **Inject** — hive-c0re exposes each container's parent (if any) to its own
|
||||||
environment as `HIVE_PARENT`, so the harness / system-prompt
|
environment as `HIVE_PARENT`, so the harness / system-prompt
|
||||||
renderer can see it.
|
renderer can see it.
|
||||||
|
|
@ -90,7 +90,6 @@ umount-old / mount-new / restart-cascade step.
|
||||||
| operation | who can do it |
|
| operation | who can do it |
|
||||||
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||||
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
|
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
|
||||||
| `request_init_config` (spawn a new child) | any agent, child added under self |
|
|
||||||
| config change via forge PR (any descendant's config) | any ancestor |
|
| config change via forge PR (any descendant's config) | any ancestor |
|
||||||
| `get_logs` (any descendant) | any ancestor |
|
| `get_logs` (any descendant) | any ancestor |
|
||||||
| moderate reminders (cancel any open thread of a descendant) | any ancestor |
|
| moderate reminders (cancel any open thread of a descendant) | any ancestor |
|
||||||
|
|
@ -110,15 +109,15 @@ other agents don't:
|
||||||
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
||||||
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
||||||
`hive-c0re` spawns it directly at boot if missing, with no operator
|
`hive-c0re` spawns it directly at boot if missing, with no operator
|
||||||
approval step — every other agent goes through `request_init_config`
|
approval step — every other agent goes through a `Spawn` approval.
|
||||||
→ approval. Topology-wise, `ruth` is still just another root agent.
|
Topology-wise, `ruth` is still just another root agent.
|
||||||
- **Wire-protocol** — the privileged `Request` variants
|
- **Wire-protocol** — the privileged `Request` variants
|
||||||
(`RequestInitConfig`; `Kill` / `Start` / `Restart` / `Update`;
|
(`Kill` / `Start` / `Restart` / `Update`;
|
||||||
`GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in
|
`GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in
|
||||||
`hive-core-agent-sock`'s unified `Request` enum — are reachable only
|
`hive-core-agent-sock`'s unified `Request` enum — are reachable only
|
||||||
from the manager's socket flavour today. Planned rule for each is in the
|
from the manager's socket flavour today. Planned rule for each is in the
|
||||||
table above ("any agent, child added under self" for init-config,
|
table above ("any ancestor" for lifecycle/logs);
|
||||||
"any ancestor" for lifecycle/logs); `RequestUpdateMetaInputs` stays
|
`RequestUpdateMetaInputs` stays
|
||||||
a root-only capability even post-milestone, not a topology rule.
|
a root-only capability even post-milestone, not a topology rule.
|
||||||
One exception: `Wake` (inject a `from: <X>` message into the
|
One exception: `Wake` (inject a `from: <X>` message into the
|
||||||
caller's own inbox) isn't really privileged — every per-agent daemon
|
caller's own inbox) isn't really privileged — every per-agent daemon
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,14 @@ CLI) before it takes effect. What you'll see, and what to do with it:
|
||||||
anything in that chain fails, the change rolls back automatically —
|
anything in that chain fails, the change rolls back automatically —
|
||||||
the agent stays on its last-good config, no recovery action needed
|
the agent stays on its last-good config, no recovery action needed
|
||||||
from you.
|
from you.
|
||||||
- **New agent** (`InitConfig` then `Spawn`) — creating a brand-new
|
- **New agent** (`Spawn`) — the one approval in creating a brand-new
|
||||||
agent is two approvals. `InitConfig` creates the config repo and
|
agent. Its config repo is scaffolded first, outside the approval
|
||||||
seeds it from a template; `Spawn` creates the container from that
|
queue, by the swarm controller's `InitAgentConfigRepo` job
|
||||||
config. Tailoring the template first isn't a separate mechanism —
|
(`POST /api/agents`); `Spawn` then creates the container from that
|
||||||
it's the config-change flow above, a PR you review like any other. Every later change goes through the config-change flow
|
config. Tailoring what the template seeded isn't a separate
|
||||||
above — there's no repeat "spawn" for an existing agent.
|
mechanism — it's the config-change flow above, a PR you review like
|
||||||
|
any other. Every later change goes through that flow — there's no
|
||||||
|
repeat "spawn" for an existing agent.
|
||||||
- **Meta/flake update** (`UpdateMetaInputs`) — an agent asked to bump
|
- **Meta/flake update** (`UpdateMetaInputs`) — an agent asked to bump
|
||||||
one or more Nix flake inputs (or all of them). Approving runs the
|
one or more Nix flake inputs (or all of them). Approving runs the
|
||||||
update and commits the lock change; it doesn't rebuild anything by
|
update and commits the lock change; it doesn't rebuild anything by
|
||||||
|
|
@ -131,11 +133,10 @@ agent that lacks the `approvals` tool group: only an agent with that
|
||||||
group submits approvals (for its direct children), so an agent
|
group submits approvals (for its direct children), so an agent
|
||||||
without it has nothing of its own to withdraw.
|
without it has nothing of its own to withdraw.
|
||||||
|
|
||||||
`InitConfig` approvals create a brand-new agent's config repo. On
|
A brand-new agent's config repo is created outside this queue, by the
|
||||||
approve, hive-c0re seeds it with a default `agent.nix` template and
|
swarm controller's `InitAgentConfigRepo` job, which seeds it with a
|
||||||
pushes a todo (`push_todo_submitter`) into the submitting agent's
|
default `agent.nix` template. The operator then **spawns** the agent
|
||||||
in-container store. The operator then **spawns** the agent (the
|
(the `Spawn` approval / `◆ R3QU3ST SP4WN` button), which creates the
|
||||||
`Spawn` approval / `◆ R3QU3ST SP4WN` button), which creates the
|
|
||||||
container from that config.
|
container from that config.
|
||||||
|
|
||||||
Changing what the template seeded isn't a special case: like every
|
Changing what the template seeded isn't a special case: like every
|
||||||
|
|
@ -146,7 +147,7 @@ through the web UI or the forge.
|
||||||
|
|
||||||
### Approval kinds (wire shapes)
|
### Approval kinds (wire shapes)
|
||||||
|
|
||||||
`ApprovalKind` carries five variants; each maps to a different
|
`ApprovalKind` carries four variants; each maps to a different
|
||||||
`commit_ref` encoding because that field is overloaded as the
|
`commit_ref` encoding because that field is overloaded as the
|
||||||
kind-specific payload carrier.
|
kind-specific payload carrier.
|
||||||
|
|
||||||
|
|
@ -169,13 +170,11 @@ kind-specific payload carrier.
|
||||||
`hivectl agent <name> request-create` CLI). The host-level `HostRequest::Spawn`
|
`hivectl agent <name> request-create` CLI). The host-level `HostRequest::Spawn`
|
||||||
variant bypasses the approval queue entirely — privileged-context use
|
variant bypasses the approval queue entirely — privileged-context use
|
||||||
only (operator on the host shell, test scripts, one-off recoveries;
|
only (operator on the host shell, test scripts, one-off recoveries;
|
||||||
`hivectl agent <name> create`). This is the **canonical first-spawn**: a new agent's `InitConfig`
|
`hivectl agent <name> create`). This is the **canonical first-spawn**: the
|
||||||
seeds its config repo, the submitting agent customises it, then the
|
swarm controller's `InitAgentConfigRepo` job seeds the agent's config
|
||||||
operator spawns to create the container. Subsequent config changes go
|
repo, it gets customised through a PR, then the operator spawns to
|
||||||
through a `MergeConfigPr` PR.
|
create the container. Subsequent config changes go through a
|
||||||
- `InitConfig` — `commit_ref` is empty; the variant just gates
|
`MergeConfigPr` PR.
|
||||||
"seed the proposed repo with the default template" against
|
|
||||||
operator approval. Step 1 of the two-step spawn flow above.
|
|
||||||
- `UpdateMetaInputs` — `commit_ref` stores the JSON-encoded inputs
|
- `UpdateMetaInputs` — `commit_ref` stores the JSON-encoded inputs
|
||||||
array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs,
|
array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs,
|
||||||
etc.). hive-c0re sets the `agent` field to the requesting root agent.
|
etc.). hive-c0re sets the `agent` field to the requesting root agent.
|
||||||
|
|
@ -421,7 +420,6 @@ rather than run inline:
|
||||||
| `MergeConfigPr` | `rebuild` (`DeployWindow` root + `MergeVerify → DeployApply` + `DeployTail`) | `approval` |
|
| `MergeConfigPr` | `rebuild` (`DeployWindow` root + `MergeVerify → DeployApply` + `DeployTail`) | `approval` |
|
||||||
| `UpdateMetaInputs` | `meta_update` (`MetaLock` + rebuild fan-out) | `approval` |
|
| `UpdateMetaInputs` | `meta_update` (`MetaLock` + rebuild fan-out) | `approval` |
|
||||||
| `Spawn` | `spawn` (`Create → WriteDropin → Reconcile`) | `approval` |
|
| `Spawn` | `spawn` (`Create → WriteDropin → Reconcile`) | `approval` |
|
||||||
| `InitConfig` | — runs inline (sub-second git seed) | — |
|
|
||||||
| `SchedulePrompt` | — runs inline (single sqlite insert) | — |
|
| `SchedulePrompt` | — runs inline (single sqlite insert) | — |
|
||||||
|
|
||||||
The DAG carries the originating `approval_id`, surfaced on the node that
|
The DAG carries the originating `approval_id`, surfaced on the node that
|
||||||
|
|
@ -604,7 +602,7 @@ as a regular `system` inbox message so it drives a normal claude turn.
|
||||||
through `Coordinator::push_todo`/`push_todo_submitter` instead, a direct
|
through `Coordinator::push_todo`/`push_todo_submitter` instead, a direct
|
||||||
live dial of the target agent's in-container todo socket (same
|
live dial of the target agent's in-container todo socket (same
|
||||||
`UpsertTodo` request in-container producers use); `finish_approval` fires
|
`UpsertTodo` request in-container producers use); `finish_approval` fires
|
||||||
one of these too for `InitConfig`/`Spawn`/`MergeConfigPr`, *in addition to*
|
one of these too for `Spawn`/`MergeConfigPr`, *in addition to*
|
||||||
the `ApprovalResolved` HelperEvent above, not instead of it. Legacy
|
the `ApprovalResolved` HelperEvent above, not instead of it. Legacy
|
||||||
approval rows that predate the submitter column fall back to the
|
approval rows that predate the submitter column fall back to the
|
||||||
root agent. Variants (`hive_sh4re::manager::HelperEvent`):
|
root agent. Variants (`hive_sh4re::manager::HelperEvent`):
|
||||||
|
|
@ -625,7 +623,7 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`):
|
||||||
no approval required.
|
no approval required.
|
||||||
|
|
||||||
The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`,
|
The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`,
|
||||||
`Destroyed`, `NeedsLogin`, `LoggedIn`, `ConfigReady` — are "FYI, check
|
`Destroyed`, `NeedsLogin`, `LoggedIn` — are "FYI, check
|
||||||
when convenient" events with no reason to drive an immediate turn, so
|
when convenient" events with no reason to drive an immediate turn, so
|
||||||
they deliver via `push_todo`/`push_todo_submitter` (see above) instead
|
they deliver via `push_todo`/`push_todo_submitter` (see above) instead
|
||||||
of `HelperEvent`: an `agent_todo_socket` push instead of a broker
|
of `HelperEvent`: an `agent_todo_socket` push instead of a broker
|
||||||
|
|
@ -638,7 +636,7 @@ hive-c0re-vouched commit sha. Optional `tag` carries the deploy
|
||||||
bookkeeping tag — `deployed/<id>` on a successful build or
|
bookkeeping tag — `deployed/<id>` on a successful build or
|
||||||
`failed/<id>` on a failed one, planted by the `MergeConfigPr` deploy.
|
`failed/<id>` on a failed one, planted by the `MergeConfigPr` deploy.
|
||||||
Both fields are `Option`: `None` on the paths that don't deploy a new
|
Both fields are `Option`: `None` on the paths that don't deploy a new
|
||||||
commit (spawn / init_config / meta-update / deny, and the autoupdate
|
commit (spawn / meta-update / deny, and the autoupdate
|
||||||
sweep's `job_queue::templates::rebuild` reapplying the existing main,
|
sweep's `job_queue::templates::rebuild` reapplying the existing main,
|
||||||
or the dashboard `↻ R3BU1LD` button when the lock didn't move). When set,
|
or the dashboard `↻ R3BU1LD` button when the lock didn't move). When set,
|
||||||
`git show <sha>` against `/applied/<n>/.git` inside the
|
`git show <sha>` against `/applied/<n>/.git` inside the
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ power-intent registry:
|
||||||
below](#state-dirs-per-agent) for where reminders (and todos)
|
below](#state-dirs-per-agent) for where reminders (and todos)
|
||||||
actually live now.
|
actually live now.
|
||||||
- `approvals` — the queue. `agent / kind (merge_config_pr | spawn |
|
- `approvals` — the queue. `agent / kind (merge_config_pr | spawn |
|
||||||
init_config | update_meta_inputs | schedule_prompt) /
|
update_meta_inputs | schedule_prompt) /
|
||||||
commit_ref / requested_at / status / resolved_at / note`.
|
commit_ref / requested_at / status / resolved_at / note`.
|
||||||
- `scheduled_prompts` — recurring + one-shot prompt queue.
|
- `scheduled_prompts` — recurring + one-shot prompt queue.
|
||||||
`owner / body / interval_seconds (NULL = one-shot) /
|
`owner / body / interval_seconds (NULL = one-shot) /
|
||||||
|
|
@ -353,9 +353,9 @@ tree anyone edits in place. Mounting it writable would leave a second
|
||||||
path to the same file that skips the review entirely, which makes the
|
path to the same file that skips the review entirely, which makes the
|
||||||
boundary a convention rather than a permission.
|
boundary a convention rather than a permission.
|
||||||
|
|
||||||
⚠️ Not to be confused with the seeding done when an `InitConfig`
|
⚠️ Not to be confused with the config-repo seeding hive-c0re does at
|
||||||
approval resolves: that writes the child's initial config repo as
|
spawn (`lifecycle::setup_proposed`): that writes the child's initial
|
||||||
**hive-c0re, against the host path**, and `read_only` on a bind
|
config repo as **hive-c0re, against the host path**, and `read_only` on a bind
|
||||||
constrains writers *inside* a container only. The two are unrelated —
|
constrains writers *inside* a container only. The two are unrelated —
|
||||||
conflating them can lead you to reason your way into thinking this
|
conflating them can lead you to reason your way into thinking this
|
||||||
mount should be writable when it shouldn't.
|
mount should be writable when it shouldn't.
|
||||||
|
|
@ -443,7 +443,7 @@ nothing is automigrated: existing agents keep their plain dirs
|
||||||
until an explicit opt-in upgrade.
|
until an explicit opt-in upgrade.
|
||||||
|
|
||||||
- **Creation:** `lifecycle::ensure_agent_state_subvolume` runs before
|
- **Creation:** `lifecycle::ensure_agent_state_subvolume` runs before
|
||||||
hive-c0re creates the per-agent subdirs (spawn / rebuild / InitConfig).
|
hive-c0re creates the per-agent subdirs (spawn / rebuild).
|
||||||
It skips the work when the root already exists; otherwise it asks
|
It skips the work when the root already exists; otherwise it asks
|
||||||
hive-priv (`EnsureAgentSubvolume`) to `btrfs subvolume create` the
|
hive-priv (`EnsureAgentSubvolume`) to `btrfs subvolume create` the
|
||||||
root when the FS is btrfs (`statfs` magic gate) and chown it to the
|
root when the FS is btrfs (`statfs` magic gate) and chown it to the
|
||||||
|
|
|
||||||
|
|
@ -247,18 +247,17 @@ a manual `hivectl` step — see _Swarm SSO_ above (`swarmctl user add`).
|
||||||
|
|
||||||
### 7 · Spawn sub-agents
|
### 7 · Spawn sub-agents
|
||||||
|
|
||||||
Sub-agent creation goes through the approval queue — ruth proposes, the
|
Sub-agent creation is an operator action — agents have no tool for it.
|
||||||
operator approves, the container builds. From ruth's own turn (inside
|
Two steps:
|
||||||
the container, via MCP tools):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Step 1: initialise a new agent's config repo
|
# Step 1: scaffold the new agent's config repo. The swarm controller's
|
||||||
request_init_config(name: "iris")
|
# InitAgentConfigRepo job does this (POST /api/agents), seeding
|
||||||
# → operator approves → config_ready event lands in the inbox
|
# /agents/iris/config/agent.nix from the default template.
|
||||||
|
|
||||||
# Step 2: edit /agents/iris/config/agent.nix and commit it. Then the
|
# Step 2: edit /agents/iris/config/agent.nix and commit it. Then spawn
|
||||||
# operator spawns iris (dashboard ◆ R3QU3ST SP4WN / Spawn approval),
|
# iris from the dashboard (◆ R3QU3ST SP4WN / Spawn approval), which
|
||||||
# which builds + starts the container from that config.
|
# builds + starts the container from that config.
|
||||||
|
|
||||||
# Later config changes: open a PR on agent-configs/iris (hive-forge);
|
# Later config changes: open a PR on agent-configs/iris (hive-forge);
|
||||||
# the operator reviews + approves it — no MCP tool call.
|
# the operator reviews + approves it — no MCP tool call.
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ binary flavor.
|
||||||
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
|
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
|
||||||
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. |
|
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. |
|
||||||
| `lifecycle` | `kill`, `start`, `restart`, `update`, `list_containers` *(privileged)* |
|
| `lifecycle` | `kill`, `start`, `restart`, `update`, `list_containers` *(privileged)* |
|
||||||
| `approvals` | `request_init_config`, `request_update_meta_inputs` *(privileged)* |
|
| `approvals` | `request_update_meta_inputs` *(privileged)* |
|
||||||
| `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* |
|
| `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* |
|
||||||
| `diagnostics` | `get_logs` *(privileged)* |
|
| `diagnostics` | `get_logs` *(privileged)* |
|
||||||
| `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) |
|
| `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) |
|
||||||
|
|
|
||||||
|
|
@ -39,38 +39,20 @@ one-row answer naming itself.
|
||||||
|
|
||||||
## `approvals` tool group
|
## `approvals` tool group
|
||||||
|
|
||||||
Config changes and new-agent spawns route through the operator
|
Meta-flake input bumps route through the operator approval queue.
|
||||||
approval queue. Topology-enforced the same way.
|
|
||||||
|
|
||||||
### `request_init_config(name, description?)`
|
Creating a new agent is **not** in this group — agents have no tool for
|
||||||
|
it. A new agent's config repo is scaffolded by the swarm controller's
|
||||||
|
`InitAgentConfigRepo` job (`POST /api/agents`, see
|
||||||
|
`swarm-controller/`), and the operator spawns the container from the
|
||||||
|
dashboard (`◆ R3QU3ST SP4WN` / `Spawn` approval, routed via
|
||||||
|
`HostRequest::RequestSpawn`).
|
||||||
|
|
||||||
Step 1 of spawning a new sub-agent. Queues an `InitConfig`
|
Config changes on an existing agent go through a **forge PR** on the
|
||||||
approval; on operator approve, hive-c0re seeds the proposed config
|
agent's `agent-configs/<name>` repo (queues a `MergeConfigPr` approval
|
||||||
repo at `/agents/<name>/config/agent.nix` with a default template and
|
on open/update — no MCP tool involved), not a tool call. See
|
||||||
delivers a `config_ready` system event. Then edit `agent.nix`, commit,
|
|
||||||
and the operator **spawns** the agent (the dashboard `◆ R3QU3ST SP4WN`
|
|
||||||
button / `Spawn` approval, routed via `HostRequest::RequestSpawn`),
|
|
||||||
which creates the container from that config.
|
|
||||||
|
|
||||||
Subsequent config changes go through a **forge PR** on the agent's
|
|
||||||
`agent-configs/<name>` repo (queues a `MergeConfigPr` approval on
|
|
||||||
open/update — no MCP tool involved), not a tool call. See
|
|
||||||
`docs/agent-lifecycle/approvals.md`.
|
`docs/agent-lifecycle/approvals.md`.
|
||||||
|
|
||||||
`name` must be either unused — in which case the caller becomes its
|
|
||||||
parent on approval — or an agent already in the caller's subtree whose
|
|
||||||
config is being re-seeded. The server refuses a name that exists outside
|
|
||||||
that subtree, so one agent can't hijack another's.
|
|
||||||
|
|
||||||
Only **direct** children's config repos are bind-mounted into a parent's
|
|
||||||
container, though (`bind_child_agent_dirs`, driven by
|
|
||||||
`topology::children_of`). The server accepts re-seeding an agent further
|
|
||||||
down the subtree, and that still leaves the caller without a local copy
|
|
||||||
to edit afterwards.
|
|
||||||
|
|
||||||
Fails if a proposed config repo for `name` already exists.
|
|
||||||
`name` is ≤ 9 characters.
|
|
||||||
|
|
||||||
### `request_update_meta_inputs(inputs?, description?)`
|
### `request_update_meta_inputs(inputs?, description?)`
|
||||||
|
|
||||||
Queue an approval to run `nix flake update [inputs...]` on the meta
|
Queue an approval to run `nix flake update [inputs...]` on the meta
|
||||||
|
|
@ -83,15 +65,14 @@ agents after the approval resolves.
|
||||||
|
|
||||||
## Boundary summary
|
## Boundary summary
|
||||||
|
|
||||||
| Operation | Requires approval? | Scope |
|
| Operation | Requires approval? | Scope |
|
||||||
| --------------------------------------- | ------------------ | ---------------------------------- |
|
| --------------------------------------- | ------------------ | ---------------------------- |
|
||||||
| `kill` / `start` / `restart` / `update` | No | Own subtree |
|
| `kill` / `start` / `restart` / `update` | No | Own subtree |
|
||||||
| `list_containers` | No | Own subtree, caller included |
|
| `list_containers` | No | Own subtree, caller included |
|
||||||
| `request_init_config` | Yes (InitConfig) | Unused name, or one in own subtree |
|
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
|
||||||
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md) — full approval flow, kinds,
|
- [`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md) — full approval flow, kinds,
|
||||||
helper events (`config_ready`, `approval_resolved`), flake.lock
|
helper events (`approval_resolved`), flake.lock
|
||||||
validation.
|
validation.
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ object with an `event` discriminant field). The **submitting agent**
|
||||||
tool group for its own subtree) receives `container_crash`,
|
tool group for its own subtree) receives `container_crash`,
|
||||||
`needs_update`, and `approval_resolved` this way. The remaining, lower-urgency lifecycle
|
`needs_update`, and `approval_resolved` this way. The remaining, lower-urgency lifecycle
|
||||||
notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`,
|
notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`,
|
||||||
`logged_in`, `config_ready` — skip the inbox entirely: they land as
|
`logged_in` — skip the inbox entirely: they land as
|
||||||
todos on the submitting agent's in-container todo socket instead
|
todos on the submitting agent's in-container todo socket instead
|
||||||
(`Coordinator::push_todo`/`push_todo_submitter`, `subsystem = "core"`),
|
(`Coordinator::push_todo`/`push_todo_submitter`, `subsystem = "core"`),
|
||||||
which still wakes a turn (the todo-wake path — see [Turn
|
which still wakes a turn (the todo-wake path — see [Turn
|
||||||
|
|
|
||||||
|
|
@ -1060,7 +1060,6 @@ renderApprovals`) with three stacked sections:
|
||||||
| `merge_config_pr` | `⇒` | `merge-pr` | PR-head sha (`sha_short`) |
|
| `merge_config_pr` | `⇒` | `merge-pr` | PR-head sha (`sha_short`) |
|
||||||
| `update_meta_inputs` | `↻` | `meta-update` | — |
|
| `update_meta_inputs` | `↻` | `meta-update` | — |
|
||||||
| `schedule_prompt` | `⏱` | `schedule` | — |
|
| `schedule_prompt` | `⏱` | `schedule` | — |
|
||||||
| `init_config` | `⊕` | `init` | — |
|
|
||||||
| `spawn` | `⊕` | `spawn` | — |
|
| `spawn` | `⊕` | `spawn` | — |
|
||||||
|
|
||||||
The chip ticks live every second via a `data-requested-at`
|
The chip ticks live every second via a `data-requested-at`
|
||||||
|
|
@ -1074,8 +1073,7 @@ renderApprovals`) with three stacked sections:
|
||||||
config PR into `agent-configs/<agent>/pulls/<pr_number>` (shown
|
config PR into `agent-configs/<agent>/pulls/<pr_number>` (shown
|
||||||
only when `forge_present` and `pr_number` is set). The config diff
|
only when `forge_present` and `pr_number` is set). The config diff
|
||||||
lives on the forge PR itself — no inline diff side-panel.
|
lives on the forge PR itself — no inline diff side-panel.
|
||||||
- `init_config` / `spawn`: a one-line "container will be created"
|
- `spawn`: a one-line "container will be created" note instead.
|
||||||
note instead.
|
|
||||||
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
- **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a
|
||||||
`prompt()` for an optional reason carried to the submitting agent as
|
`prompt()` for an optional reason carried to the submitting agent as
|
||||||
`HelperEvent::ApprovalResolved.note`.
|
`HelperEvent::ApprovalResolved.note`.
|
||||||
|
|
|
||||||
|
|
@ -232,7 +232,6 @@ export function renderApprovals() {
|
||||||
|
|
||||||
const ul = el("ul", { class: "approvals" });
|
const ul = el("ul", { class: "approvals" });
|
||||||
for (const a of pending) {
|
for (const a of pending) {
|
||||||
const isInit = a.kind === "init_config";
|
|
||||||
const isMergePr = a.kind === "merge_config_pr";
|
const isMergePr = a.kind === "merge_config_pr";
|
||||||
const isUpdateMeta = a.kind === "update_meta_inputs";
|
const isUpdateMeta = a.kind === "update_meta_inputs";
|
||||||
const isSchedule = a.kind === "schedule_prompt";
|
const isSchedule = a.kind === "schedule_prompt";
|
||||||
|
|
@ -262,9 +261,7 @@ export function renderApprovals() {
|
||||||
? "meta-update"
|
? "meta-update"
|
||||||
: isSchedule
|
: isSchedule
|
||||||
? "schedule"
|
? "schedule"
|
||||||
: isInit
|
: "spawn",
|
||||||
? "init"
|
|
||||||
: "spawn",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (isMergePr && a.sha_short) head.append(el("code", {}, a.sha_short));
|
if (isMergePr && a.sha_short) head.append(el("code", {}, a.sha_short));
|
||||||
|
|
@ -440,9 +437,7 @@ function renderApprovalHistory(root, history) {
|
||||||
? "meta-update"
|
? "meta-update"
|
||||||
: a.kind === "schedule_prompt"
|
: a.kind === "schedule_prompt"
|
||||||
? "schedule"
|
? "schedule"
|
||||||
: a.kind === "init_config"
|
: "spawn",
|
||||||
? "init"
|
|
||||||
: "spawn",
|
|
||||||
),
|
),
|
||||||
" ",
|
" ",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,7 @@ window.marked = marked;
|
||||||
for (const a of approvals) {
|
for (const a of approvals) {
|
||||||
if (seenApprovals.has(a.id)) continue;
|
if (seenApprovals.has(a.id)) continue;
|
||||||
seenApprovals.add(a.id);
|
seenApprovals.add(a.id);
|
||||||
const verb =
|
const verb = a.kind === "spawn" ? "spawn approval" : "config commit";
|
||||||
a.kind === "spawn"
|
|
||||||
? "spawn approval"
|
|
||||||
: a.kind === "init_config"
|
|
||||||
? "config-init approval"
|
|
||||||
: "config commit";
|
|
||||||
NOTIF.show(
|
NOTIF.show(
|
||||||
"◆ approval #" + a.id,
|
"◆ approval #" + a.id,
|
||||||
`${verb} for ${a.agent}`,
|
`${verb} for ${a.agent}`,
|
||||||
|
|
|
||||||
|
|
@ -97,20 +97,6 @@ pub struct CompactArgs {
|
||||||
// Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics)
|
// Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
|
||||||
pub struct RequestInitConfigArgs {
|
|
||||||
/// New sub-agent name (≤9 chars). Queues an `InitConfig` approval; on
|
|
||||||
/// approval hive-c0re creates the child's config repo and seeds it with a
|
|
||||||
/// default `agent.nix`. Approving the follow-up `Spawn` creates the
|
|
||||||
/// container. Config changes — including the child's first — are PRs on
|
|
||||||
/// that repo, made from a clone, reviewed + approved by the operator;
|
|
||||||
/// `/agents/<name>/config` is a read-only copy, not an editing surface.
|
|
||||||
pub name: String,
|
|
||||||
/// Optional description shown on the dashboard approval card.
|
|
||||||
#[serde(default)]
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
pub struct KillArgs {
|
pub struct KillArgs {
|
||||||
/// Sub-agent name (without the `h-` container prefix).
|
/// Sub-agent name (without the `h-` container prefix).
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,8 @@ mod render;
|
||||||
pub use args::{
|
pub use args::{
|
||||||
AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs,
|
AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs,
|
||||||
CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs,
|
CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs,
|
||||||
GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestInitConfigArgs,
|
GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs,
|
||||||
RequestSchedulePromptArgs, RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs,
|
RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs,
|
||||||
UpdateMetaInputsArgs,
|
|
||||||
};
|
};
|
||||||
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
|
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
|
||||||
|
|
||||||
|
|
@ -710,47 +709,6 @@ impl AgentServer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: this tool is only available when the `approvals` tool group
|
|
||||||
// is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`).
|
|
||||||
// hive-c0re performs a topology check server-side: an unused `name` is
|
|
||||||
// accepted from any caller (the requester becomes its parent); an existing
|
|
||||||
// agent is accepted only from inside the caller's subtree.
|
|
||||||
#[tool(description = "Create a new agent's config repo and queue an \
|
|
||||||
`InitConfig` approval for the operator to review. Requires the `approvals` tool \
|
|
||||||
group. `name` must be either unused — in which case you become its parent — or an \
|
|
||||||
agent already in your subtree, whose config you are re-seeding. Fails if a \
|
|
||||||
config repo for that agent already exists. This tool **creates the repo** and \
|
|
||||||
nothing else: on approval hive-c0re seeds it with a default `agent.nix`, and \
|
|
||||||
approving the follow-up `Spawn` creates the container. \
|
|
||||||
Every config change — the child's first one included — goes through a PR on its \
|
|
||||||
config repo, made from a clone you take yourself, reviewed + approved by the \
|
|
||||||
operator. `/agents/<name>/config` is a **read-only copy** for reading a config, \
|
|
||||||
never an editing surface.")]
|
|
||||||
async fn request_init_config(
|
|
||||||
&self,
|
|
||||||
Parameters(args): Parameters<RequestInitConfigArgs>,
|
|
||||||
) -> String {
|
|
||||||
let log = format!("{args:?}");
|
|
||||||
let name = args.name.clone();
|
|
||||||
run_tool_envelope("request_init_config", log, async move {
|
|
||||||
let (resp, retries) = self
|
|
||||||
.dispatch(hive_core_agent_sock::Request::RequestInitConfig {
|
|
||||||
name: args.name,
|
|
||||||
description: args.description,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
annotate_retries(
|
|
||||||
format_ack(
|
|
||||||
resp,
|
|
||||||
"request_init_config",
|
|
||||||
format!("init_config approval queued for {name}"),
|
|
||||||
),
|
|
||||||
retries,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: this tool is only available when the `lifecycle` tool group
|
// IMPORTANT: this tool is only available when the `lifecycle` tool group
|
||||||
// is granted to this agent. hive-c0re enforces the topology check
|
// is granted to this agent. hive-c0re enforces the topology check
|
||||||
// server-side: the call is rejected unless `name` is in the caller's
|
// server-side: the call is rejected unless `name` is in the caller's
|
||||||
|
|
|
||||||
|
|
@ -643,12 +643,12 @@ mod tests {
|
||||||
Ok(hive_core_agent_sock::Response::OkWarn {
|
Ok(hive_core_agent_sock::Response::OkWarn {
|
||||||
warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()],
|
warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()],
|
||||||
}),
|
}),
|
||||||
"request_init_config",
|
"request_update_meta_inputs",
|
||||||
"init_config approval queued for forge".to_owned(),
|
"update_meta_inputs approval queued".to_owned(),
|
||||||
);
|
);
|
||||||
// The operation HAPPENED — dropping the success line would read as a
|
// The operation HAPPENED — dropping the success line would read as a
|
||||||
// failure and invite a retry that queues a second approval.
|
// failure and invite a retry that queues a second approval.
|
||||||
assert!(out.starts_with("init_config approval queued for forge"));
|
assert!(out.starts_with("update_meta_inputs approval queued"));
|
||||||
assert!(out.contains("⚠️ name is reserved"));
|
assert!(out.contains("⚠️ name is reserved"));
|
||||||
assert!(out.contains("⚠️ second thing"));
|
assert!(out.contains("⚠️ second thing"));
|
||||||
}
|
}
|
||||||
|
|
@ -659,7 +659,7 @@ mod tests {
|
||||||
// warning marker would pass the test above.
|
// warning marker would pass the test above.
|
||||||
let out = format_ack(
|
let out = format_ack(
|
||||||
Ok(hive_core_agent_sock::Response::Ok),
|
Ok(hive_core_agent_sock::Response::Ok),
|
||||||
"request_init_config",
|
"request_update_meta_inputs",
|
||||||
"queued".to_owned(),
|
"queued".to_owned(),
|
||||||
);
|
);
|
||||||
assert_eq!(out, "queued");
|
assert_eq!(out, "queued");
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,23 @@ Tools (hyperhive surface). Full signature + behavior for each comes from the too
|
||||||
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at.
|
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at.
|
||||||
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
|
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
|
||||||
- **Lifecycle** (_requires `lifecycle` tool group_, your own subtree — children, their children, and so on down, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`.
|
- **Lifecycle** (_requires `lifecycle` tool group_, your own subtree — children, their children, and so on down, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`.
|
||||||
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_update_meta_inputs`.
|
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_update_meta_inputs`.
|
||||||
- **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns).
|
- **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns).
|
||||||
- **Diagnostics**: `get_logs` (_requires `diagnostics` tool group_), `get_host_journal` (_requires `read_host_journal` capability_).
|
- **Diagnostics**: `get_logs` (_requires `diagnostics` tool group_), `get_host_journal` (_requires `read_host_journal` capability_).
|
||||||
|
|
||||||
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits.
|
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — open a PR on your `agent-configs/<name>` repo, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits.
|
||||||
|
|
||||||
Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line.
|
Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line.
|
||||||
|
|
||||||
Approval boundary: lifecycle ops on _existing_ agents in your subtree (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent starts with `request_init_config` (requires `approvals` tool group), then the operator spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/<name>` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day.
|
Approval boundary: lifecycle ops on _existing_ agents in your subtree (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent is not something you can do from here — ask the operator, who scaffolds the new agent's config repo and spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/<name>` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day.
|
||||||
|
|
||||||
Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes:
|
Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes:
|
||||||
|
|
||||||
- `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval.
|
- `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval.
|
||||||
- `container_crash` — restart with `start(name)`. If it crashes again, ask the operator.
|
- `container_crash` — restart with `start(name)`. If it crashes again, ask the operator.
|
||||||
- `approval_resolved` — one of your own submitted approvals (`request_init_config`, `request_update_meta_inputs`, a scheduled prompt, …) was approved, denied, or failed; the body carries the resolution.
|
- `approval_resolved` — one of your own submitted approvals (`request_update_meta_inputs`, a scheduled prompt, a config PR, …) was approved, denied, or failed; the body carries the resolution.
|
||||||
|
|
||||||
Lifecycle notices that don't need an immediate turn — a new agent spawned, its config repo seeded, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them.
|
Lifecycle notices that don't need an immediate turn — a new agent spawned, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them.
|
||||||
|
|
||||||
Durable knowledge:
|
Durable knowledge:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -646,8 +646,7 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String {
|
||||||
"mcp__hyperhive__kill"
|
"mcp__hyperhive__kill"
|
||||||
| "mcp__hyperhive__restart"
|
| "mcp__hyperhive__restart"
|
||||||
| "mcp__hyperhive__start"
|
| "mcp__hyperhive__start"
|
||||||
| "mcp__hyperhive__update"
|
| "mcp__hyperhive__update" => format!("{short} {}", sv(input, "name")),
|
||||||
| "mcp__hyperhive__request_init_config" => format!("{short} {}", sv(input, "name")),
|
|
||||||
"mcp__hyperhive__ack_until" => {
|
"mcp__hyperhive__ack_until" => {
|
||||||
let up_to = input
|
let up_to = input
|
||||||
.get("up_to")
|
.get("up_to")
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,10 @@ use hive_sh4re::manager::HelperEvent;
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
||||||
/// Approve a pending request. Marks the approval row durably, then
|
/// Approve a pending request. Marks the approval row durably, then submits
|
||||||
/// either runs the work inline (`InitConfig`, sub-second git ops) or
|
/// the work to the job queue so the dashboard POST returns immediately while
|
||||||
/// submits it to the job queue so the dashboard POST returns
|
/// the long-running pipeline runs off-thread (operator no longer blocks on a
|
||||||
/// immediately while the long-running pipeline runs off-thread
|
/// 30-90s spinner for `MergeConfigPr`).
|
||||||
/// (operator no longer blocks on a 30-90s spinner for `MergeConfigPr`).
|
|
||||||
///
|
///
|
||||||
/// Dispatch:
|
/// Dispatch:
|
||||||
/// - `MergeConfigPr` → a `DeployWindow` DAG (`MergeVerify → DeployApply →
|
/// - `MergeConfigPr` → a `DeployWindow` DAG (`MergeVerify → DeployApply →
|
||||||
|
|
@ -24,7 +23,6 @@ use crate::lifecycle;
|
||||||
/// resource-holding root; ~30-90s)
|
/// resource-holding root; ~30-90s)
|
||||||
/// - `UpdateMetaInputs` → a `MetaUpdate` DAG (fan-out on completion)
|
/// - `UpdateMetaInputs` → a `MetaUpdate` DAG (fan-out on completion)
|
||||||
/// - `Spawn` → a `Spawn` DAG (`Create → WriteDropin → Reconcile`)
|
/// - `Spawn` → a `Spawn` DAG (`Create → WriteDropin → Reconcile`)
|
||||||
/// - `InitConfig` → inline (<1s; queue card would be noise)
|
|
||||||
///
|
///
|
||||||
/// Every queued kind — deploys included — resolves its approval row via
|
/// Every queued kind — deploys included — resolves its approval row via
|
||||||
/// [`resolve_approval_dag`] when the DAG settles terminal.
|
/// [`resolve_approval_dag`] when the DAG settles terminal.
|
||||||
|
|
@ -38,15 +36,6 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
||||||
"approval: dispatching",
|
"approval: dispatching",
|
||||||
);
|
);
|
||||||
match approval.kind {
|
match approval.kind {
|
||||||
ApprovalKind::InitConfig => {
|
|
||||||
// Sub-second git seed + forge-remote wire. Routing through
|
|
||||||
// the queue would surface a queue card that's gone before
|
|
||||||
// the operator's eyes refocus. Run inline.
|
|
||||||
let proposed_dir = Coordinator::agent_proposed_dir(&approval.agent);
|
|
||||||
let claude_dir = Coordinator::agent_claude_dir(&approval.agent);
|
|
||||||
let notes_dir = Coordinator::agent_notes_dir(&approval.agent);
|
|
||||||
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
|
||||||
}
|
|
||||||
ApprovalKind::UpdateMetaInputs => {
|
ApprovalKind::UpdateMetaInputs => {
|
||||||
// Inputs JSON-encoded into commit_ref by the manager's
|
// Inputs JSON-encoded into commit_ref by the manager's
|
||||||
// submit path — surface them on the DAG so the dashboard
|
// submit path — surface them on the DAG so the dashboard
|
||||||
|
|
@ -638,49 +627,6 @@ async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
|
||||||
crate::dashboard::emit_tombstones_snapshot(coord).await;
|
crate::dashboard::emit_tombstones_snapshot(coord).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inline (non-queued) handler for `ApprovalKind::InitConfig`. Just
|
|
||||||
/// seeds the proposed git repo + the per-agent dirs — sub-second
|
|
||||||
/// work that doesn't justify a queue card.
|
|
||||||
async fn run_approval_init_config(
|
|
||||||
coord: &Coordinator,
|
|
||||||
approval: hive_sh4re::approvals::Approval,
|
|
||||||
proposed_dir: std::path::PathBuf,
|
|
||||||
claude_dir: std::path::PathBuf,
|
|
||||||
notes_dir: std::path::PathBuf,
|
|
||||||
) -> Result<()> {
|
|
||||||
let result: Result<()> = async {
|
|
||||||
// Place the new child under its requesting parent (carried in
|
|
||||||
// commit_ref by submit_init_config). An empty commit_ref means
|
|
||||||
// no explicit parent was named (privileged manager socket, or an
|
|
||||||
// approval queued before the new-child feature) — write no edge
|
|
||||||
// and let `topology::reconcile` assign the default position on
|
|
||||||
// first spawn, so this path never names a specific root agent.
|
|
||||||
if !approval.commit_ref.is_empty() {
|
|
||||||
crate::topology::add_child(approval.agent.as_str(), &approval.commit_ref)
|
|
||||||
.map_err(|e| anyhow::anyhow!("topology add_child: {e}"))?;
|
|
||||||
}
|
|
||||||
// Create the agent's state root as a btrfs subvolume FIRST, before
|
|
||||||
// any dir-seed touches it. `ensure_agent_state_subvolume` is
|
|
||||||
// progressive ("root exists → skip"), and `setup_proposed` does a
|
|
||||||
// `create_dir_all` on the proposed-config path which would
|
|
||||||
// materialise the state root as a plain directory — after which
|
|
||||||
// the subvolume create is silently skipped and the agent never
|
|
||||||
// lands on a subvolume (no quota, no snapshot). Order matters.
|
|
||||||
lifecycle::ensure_agent_state_subvolume(approval.agent.as_str()).await?;
|
|
||||||
lifecycle::setup_proposed(&proposed_dir, approval.agent.as_str()).await?;
|
|
||||||
lifecycle::ensure_claude_dir(&claude_dir)?;
|
|
||||||
lifecycle::ensure_state_dir(¬es_dir)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
if result.is_ok()
|
|
||||||
&& let Err(e) = crate::forge::ensure_meta_remote(approval.agent.as_str()).await
|
|
||||||
{
|
|
||||||
tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_meta_remote after init_config failed");
|
|
||||||
}
|
|
||||||
finish_approval(coord, &approval, result, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn finish_approval(
|
async fn finish_approval(
|
||||||
coord: &Coordinator,
|
coord: &Coordinator,
|
||||||
approval: &hive_sh4re::approvals::Approval,
|
approval: &hive_sh4re::approvals::Approval,
|
||||||
|
|
@ -727,28 +673,12 @@ async fn finish_approval(
|
||||||
note: note.clone(),
|
note: note.clone(),
|
||||||
description: approval.description.clone(),
|
description: approval.description.clone(),
|
||||||
});
|
});
|
||||||
// For spawn/rebuild/init_config approvals, also surface the underlying
|
// For spawn/rebuild approvals, also surface the underlying action so the
|
||||||
// action so the manager knows whether the lifecycle step succeeded.
|
// manager knows whether the lifecycle step succeeded. The
|
||||||
// The ApprovalResolved event already carries the same `ok` signal but
|
// ApprovalResolved event already carries the same `ok` signal but
|
||||||
// separating it lets the manager react to the lifecycle change
|
// separating it lets the manager react to the lifecycle change
|
||||||
// without having to special-case approvals.
|
// without having to special-case approvals.
|
||||||
match approval.kind {
|
match approval.kind {
|
||||||
ApprovalKind::InitConfig => {
|
|
||||||
if ok {
|
|
||||||
let _ = coord
|
|
||||||
.push_todo_submitter(
|
|
||||||
approval.id,
|
|
||||||
"core",
|
|
||||||
Some(format!("config_ready:{}", approval.agent)),
|
|
||||||
format!(
|
|
||||||
"agent '{}' config repo ready — edit + apply",
|
|
||||||
approval.agent
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApprovalKind::Spawn => {
|
ApprovalKind::Spawn => {
|
||||||
let summary = if ok {
|
let summary = if ok {
|
||||||
format!("agent '{}' spawned", approval.agent)
|
format!("agent '{}' spawned", approval.agent)
|
||||||
|
|
|
||||||
|
|
@ -229,9 +229,8 @@ pub fn write(topology: &BTreeMap<String, Option<String>>) -> std::io::Result<()>
|
||||||
|
|
||||||
/// Compute the default topology for a fresh install: every agent is a
|
/// Compute the default topology for a fresh install: every agent is a
|
||||||
/// root (parent = null). There is no structural "manager" — agents
|
/// root (parent = null). There is no structural "manager" — agents
|
||||||
/// arrange themselves via explicit parent edges (an agent-requested
|
/// arrange themselves via explicit parent edges, written by the operator
|
||||||
/// sub-agent gets a requester-as-parent edge at `init_config`; the
|
/// through the dashboard / `RequestSetParent` API.
|
||||||
/// operator reparents via the dashboard / `RequestSetParent` API).
|
|
||||||
/// Used by `meta::sync_agents` on first call to seed `topology.json`.
|
/// Used by `meta::sync_agents` on first call to seed `topology.json`.
|
||||||
///
|
///
|
||||||
/// As soon as an explicit write lands (dashboard / `RequestSetParent`
|
/// As soon as an explicit write lands (dashboard / `RequestSetParent`
|
||||||
|
|
@ -298,53 +297,6 @@ pub fn apply_set_parent(
|
||||||
Ok(next)
|
Ok(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Declare a brand-new agent's parent edge before the agent exists in
|
|
||||||
/// the container set. Unlike [`crate::meta::bulk_commit_topology`] /
|
|
||||||
/// [`apply_set_parent`] (which reparent an entry that must already be
|
|
||||||
/// present), this inserts a fresh `child -> parent`
|
|
||||||
/// row. Used by the `InitConfig` approval to place a just-scaffolded
|
|
||||||
/// sub-agent under its requesting parent, so the edge is in place
|
|
||||||
/// before the first apply-commit spawns the container (and before
|
|
||||||
/// [`reconcile`] would otherwise default it to the manager).
|
|
||||||
///
|
|
||||||
/// Idempotent when the edge already exists. Refuses to clobber an entry
|
|
||||||
/// whose parent differs (one agent can't steal another's child) and
|
|
||||||
/// validates that `parent` is itself a known agent.
|
|
||||||
pub fn add_child(child: &str, parent: &str) -> Result<(), String> {
|
|
||||||
let current = read();
|
|
||||||
match apply_add_child(¤t, child, parent)? {
|
|
||||||
Some(next) => write(&next).map_err(|e| format!("write topology.json: {e}")),
|
|
||||||
None => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pure form of [`add_child`] for unit tests. Returns the post-insert
|
|
||||||
/// map (caller writes it back), `None` for an idempotent no-op (edge
|
|
||||||
/// already present), or an error string (unknown parent / name owned by
|
|
||||||
/// a different parent).
|
|
||||||
pub fn apply_add_child(
|
|
||||||
topo: &BTreeMap<String, Option<String>>,
|
|
||||||
child: &str,
|
|
||||||
parent: &str,
|
|
||||||
) -> Result<Option<BTreeMap<String, Option<String>>>, String> {
|
|
||||||
if !topo.contains_key(parent) {
|
|
||||||
return Err(format!("unknown parent: {parent}"));
|
|
||||||
}
|
|
||||||
match topo.get(child) {
|
|
||||||
Some(Some(p)) if p == parent => return Ok(None),
|
|
||||||
Some(existing) => {
|
|
||||||
return Err(format!(
|
|
||||||
"agent {child} already exists in topology (parent: {existing:?}); \
|
|
||||||
refusing to reparent via add_child"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
let mut next = topo.clone();
|
|
||||||
next.insert(child.to_owned(), Some(parent.to_owned()));
|
|
||||||
Ok(Some(next))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reconcile `topology.json` against the current agent set. Adds an
|
/// Reconcile `topology.json` against the current agent set. Adds an
|
||||||
/// entry (default: parent = null — a new agent with no declared parent
|
/// entry (default: parent = null — a new agent with no declared parent
|
||||||
/// is its own root) for any agent missing from the file; removes
|
/// is its own root) for any agent missing from the file; removes
|
||||||
|
|
@ -353,10 +305,10 @@ pub fn apply_add_child(
|
||||||
/// choices stick across regenerations. Returns true when the file
|
/// choices stick across regenerations. Returns true when the file
|
||||||
/// changed and should be re-committed by the caller.
|
/// changed and should be re-committed by the caller.
|
||||||
///
|
///
|
||||||
/// `pending` lists agents that have an operator-approved proposed config
|
/// `pending` lists agents that have a provisioned proposed config repo
|
||||||
/// repo but no container yet (init'd, not yet spawned). They are KEPT
|
/// but no container yet (provisioned, not yet spawned). They are KEPT
|
||||||
/// (not dropped) so the parent edge written at `InitConfig` approval
|
/// (not dropped) so an explicit parent edge written before the first
|
||||||
/// survives until the first apply-commit, but they are NOT seeded with a
|
/// spawn survives until the first apply-commit, but they are NOT seeded with a
|
||||||
/// default parent here and NOT added to roles — that happens when the
|
/// default parent here and NOT added to roles — that happens when the
|
||||||
/// container actually spawns and the name moves into `agent_names`.
|
/// container actually spawns and the name moves into `agent_names`.
|
||||||
pub fn reconcile(agent_names: &[String], pending: &[String]) -> std::io::Result<bool> {
|
pub fn reconcile(agent_names: &[String], pending: &[String]) -> std::io::Result<bool> {
|
||||||
|
|
@ -384,12 +336,11 @@ pub fn apply_reconcile(
|
||||||
for name in agent_names {
|
for name in agent_names {
|
||||||
if !next.contains_key(name) {
|
if !next.contains_key(name) {
|
||||||
// A new agent with no declared parent defaults to root
|
// A new agent with no declared parent defaults to root
|
||||||
// (parent = null). Agent-requested sub-agents always carry an
|
// (parent = null). An agent placed under a parent carries an
|
||||||
// explicit requester-as-parent edge (written at init_config
|
// explicit edge written before its first spawn, so it never
|
||||||
// approval), so they never hit this default — only
|
// hits this default — only spawns with no declared parent do,
|
||||||
// user/operator-initiated spawns do, and those are roots. No
|
// and those are roots. No agent is structurally privileged
|
||||||
// agent is structurally privileged here: "root-ness" is just
|
// here: "root-ness" is just a null parent.
|
||||||
// a null parent.
|
|
||||||
next.insert(name.clone(), None);
|
next.insert(name.clone(), None);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
@ -502,8 +453,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn default_seed_makes_every_agent_root() {
|
fn default_seed_makes_every_agent_root() {
|
||||||
// No structural manager: every agent defaults to root (null
|
// No structural manager: every agent defaults to root (null
|
||||||
// parent). Explicit edges (init_config / dashboard) are layered
|
// parent). Explicit edges are layered on later.
|
||||||
// on later.
|
|
||||||
let agents = vec![
|
let agents = vec![
|
||||||
"alice".to_owned(),
|
"alice".to_owned(),
|
||||||
crate::lifecycle::MANAGER_NAME.to_owned(),
|
crate::lifecycle::MANAGER_NAME.to_owned(),
|
||||||
|
|
@ -630,42 +580,6 @@ mod tests {
|
||||||
assert_eq!(next, topo_three_level());
|
assert_eq!(next, topo_three_level());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_add_child_inserts_new_edge_under_parent() {
|
|
||||||
// alice spawns a brand-new child `dora`: the edge lands
|
|
||||||
// with alice as parent without disturbing the rest of the tree.
|
|
||||||
let next = apply_add_child(&topo_three_level(), "dora", "alice")
|
|
||||||
.unwrap()
|
|
||||||
.expect("brand-new edge should produce a map");
|
|
||||||
assert_eq!(next.get("dora"), Some(&Some("alice".to_owned())));
|
|
||||||
// existing entries untouched.
|
|
||||||
assert_eq!(next.get("bob"), Some(&Some("alice".to_owned())));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_add_child_is_idempotent_when_edge_exists() {
|
|
||||||
// bob already under alice — re-init returns a no-op (None).
|
|
||||||
assert!(
|
|
||||||
apply_add_child(&topo_three_level(), "bob", "alice")
|
|
||||||
.unwrap()
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_add_child_refuses_unknown_parent() {
|
|
||||||
let err = apply_add_child(&topo_three_level(), "dora", "nobody").unwrap_err();
|
|
||||||
assert!(err.contains("unknown parent"), "err = {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_add_child_refuses_name_owned_by_other_parent() {
|
|
||||||
// bob lives under alice; the manager can't claim it via add_child.
|
|
||||||
let err = apply_add_child(&topo_three_level(), "bob", crate::lifecycle::MANAGER_NAME)
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(err.contains("already exists"), "err = {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_reconcile_adds_missing_live_agent_as_root() {
|
fn apply_reconcile_adds_missing_live_agent_as_root() {
|
||||||
// A live agent with no prior topology entry defaults to root
|
// A live agent with no prior topology entry defaults to root
|
||||||
|
|
@ -696,7 +610,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_reconcile_keeps_pending_init_agent_edge() {
|
fn apply_reconcile_keeps_pending_init_agent_edge() {
|
||||||
// `dora` was init'd under alice (edge present) but has no
|
// `dora` was placed under alice (edge present) but has no
|
||||||
// container yet, so it's absent from the live set. It must NOT
|
// container yet, so it's absent from the live set. It must NOT
|
||||||
// be dropped, and its alice-parent edge must be preserved (not
|
// be dropped, and its alice-parent edge must be preserved (not
|
||||||
// re-seeded under the manager).
|
// re-seeded under the manager).
|
||||||
|
|
|
||||||
|
|
@ -1512,8 +1512,8 @@ impl Coordinator {
|
||||||
|
|
||||||
/// Agents that have an operator-approved proposed config repo but
|
/// Agents that have an operator-approved proposed config repo but
|
||||||
/// were never deployed — proposed `.git` exists, applied `.git` does
|
/// were never deployed — proposed `.git` exists, applied `.git` does
|
||||||
/// not. Their `topology.json` parent edge (written at `InitConfig`
|
/// not. Their `topology.json` parent edge (written before the first
|
||||||
/// approval) must survive `topology::reconcile` until the first
|
/// spawn) must survive `topology::reconcile` until the first
|
||||||
/// apply-commit spawns the container. Distinct from tombstones,
|
/// apply-commit spawns the container. Distinct from tombstones,
|
||||||
/// which have an applied repo from a prior deploy.
|
/// which have an applied repo from a prior deploy.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
|
||||||
|
|
@ -83,13 +83,9 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
|
||||||
approvals
|
approvals
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|a| {
|
.filter(|a| {
|
||||||
// Spawn and InitConfig approvals are for not-yet-existent agents;
|
// A Spawn approval is for a not-yet-existent agent; the proposed
|
||||||
// the proposed dir is supposed to be missing.
|
// dir is supposed to be missing, so absence is not orphanhood.
|
||||||
if matches!(
|
if a.kind == hive_sh4re::approvals::ApprovalKind::Spawn {
|
||||||
a.kind,
|
|
||||||
hive_sh4re::approvals::ApprovalKind::Spawn
|
|
||||||
| hive_sh4re::approvals::ApprovalKind::InitConfig
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if Coordinator::agent_proposed_dir(&a.agent).exists() {
|
if Coordinator::agent_proposed_dir(&a.agent).exists() {
|
||||||
|
|
|
||||||
|
|
@ -449,16 +449,6 @@ fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
||||||
commit_ref: None,
|
commit_ref: None,
|
||||||
requested_at: a.requested_at,
|
requested_at: a.requested_at,
|
||||||
},
|
},
|
||||||
hive_sh4re::approvals::ApprovalKind::InitConfig => ApprovalView {
|
|
||||||
id: a.id,
|
|
||||||
agent: a.agent.to_string(),
|
|
||||||
kind: "init_config",
|
|
||||||
sha_short: None,
|
|
||||||
description: a.description,
|
|
||||||
pr_number: None,
|
|
||||||
commit_ref: None,
|
|
||||||
requested_at: a.requested_at,
|
|
||||||
},
|
|
||||||
hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs => ApprovalView {
|
hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs => ApprovalView {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
agent: a.agent.to_string(),
|
agent: a.agent.to_string(),
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,10 @@ fn config_bind_source(name: &str) -> PathBuf {
|
||||||
/// hive-c0re still reads it directly on the host (`stats::hive_stats`),
|
/// hive-c0re still reads it directly on the host (`stats::hive_stats`),
|
||||||
/// which needs no bind mount into the parent.
|
/// which needs no bind mount into the parent.
|
||||||
///
|
///
|
||||||
/// ⚠️ The seeding done at `InitConfig` approval is **not** affected by the
|
/// ⚠️ The config-repo seeding hive-c0re does at spawn is **not** affected by
|
||||||
/// `config` flag and must not be read as a reason to widen it: that runs
|
/// the `config` flag and must not be read as a reason to widen it: that runs
|
||||||
/// as hive-c0re against the host path (see `actions.rs`, which seeds the
|
/// as hive-c0re against the host path (see `lifecycle::setup_proposed`), and
|
||||||
/// repo and wires its forge remote inline), and `read_only` on a bind
|
/// `read_only` on a bind constrains writers *inside* the container only.
|
||||||
/// constrains writers *inside* the container only.
|
|
||||||
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
||||||
let Ok(child) = hive_types::Ident::parse(child) else {
|
let Ok(child) = hive_types::Ident::parse(child) else {
|
||||||
tracing::warn!(%child, "skipping child bind: invalid agent name");
|
tracing::warn!(%child, "skipping child bind: invalid agent name");
|
||||||
|
|
|
||||||
|
|
@ -171,8 +171,6 @@ pub async fn setup_applied(
|
||||||
/// valid session; credential files inside (`.credentials.json` etc.) are 0600 so
|
/// valid session; credential files inside (`.credentials.json` etc.) are 0600 so
|
||||||
/// secrets stay private regardless of the directory mode. Idempotent: existing
|
/// secrets stay private regardless of the directory mode. Idempotent: existing
|
||||||
/// dirs are left untouched (an agent's OAuth tokens survive `destroy`/recreate).
|
/// dirs are left untouched (an agent's OAuth tokens survive `destroy`/recreate).
|
||||||
/// Public for the `InitConfig` approval path in `actions.rs` which seeds
|
|
||||||
/// dirs without calling the full `spawn`.
|
|
||||||
pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
||||||
use std::io;
|
use std::io;
|
||||||
if !claude_dir.exists() {
|
if !claude_dir.exists() {
|
||||||
|
|
@ -208,9 +206,9 @@ pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public for the `InitConfig` approval path in `actions.rs` which seeds
|
/// Create the per-agent state dir if missing. Also creates the sibling
|
||||||
/// dirs without calling the full `spawn`. Also creates the sibling `harness/`
|
/// `harness/` dir so the first harness startup can write its sqlite files
|
||||||
/// dir so the first harness startup can write its sqlite files immediately.
|
/// immediately.
|
||||||
pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
||||||
if !notes_dir.exists() {
|
if !notes_dir.exists() {
|
||||||
std::fs::create_dir_all(notes_dir)
|
std::fs::create_dir_all(notes_dir)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! Config-approval request handlers: `RequestInitConfig` /
|
//! Config-approval request handlers: `RequestUpdateMetaInputs`, plus the
|
||||||
//! `RequestUpdateMetaInputs`, plus the shared submit helpers
|
//! shared submit helper `submit_merge_config_pr`.
|
||||||
//! (`submit_init_config` / `submit_merge_config_pr`).
|
|
||||||
//!
|
//!
|
||||||
//! `submit_merge_config_pr` is called from the dashboard webhook handler
|
//! `submit_merge_config_pr` is called from the dashboard webhook handler
|
||||||
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config
|
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config
|
||||||
|
|
@ -11,70 +10,8 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use hive_core_agent_sock::Response;
|
use hive_core_agent_sock::Response;
|
||||||
|
|
||||||
use super::require_new_child;
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
|
|
||||||
/// `RequestInitConfig` — queue an `InitConfig` approval for an agent. The
|
|
||||||
/// `name` must be brand-new (absent from the topology) or already in the
|
|
||||||
/// caller's subtree; the requester is recorded as the new agent's parent (the
|
|
||||||
/// root requesting a new agent → a top-level agent, matching reconcile's
|
|
||||||
/// default).
|
|
||||||
pub(super) fn handle_request_init_config(
|
|
||||||
coord: &Arc<Coordinator>,
|
|
||||||
agent: &str,
|
|
||||||
name: &str,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> Response {
|
|
||||||
if let Some(err) = require_new_child(agent, name, "request_init_config for") {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
tracing::info!(%agent, %name, "request_init_config");
|
|
||||||
// Warn, do not refuse: an agent already created under a colliding
|
|
||||||
// name must stay re-initialisable, so the refusal comes later, once
|
|
||||||
// the warning has had time to be seen.
|
|
||||||
//
|
|
||||||
// Checked HERE and not only in `swarm-controller::create_agent`:
|
|
||||||
// that daemon is opt-in and off on most hives, while this is the
|
|
||||||
// path the `request_init_config` tool takes on every hive. Guarding
|
|
||||||
// only the rarer one would have left the common flow exactly as
|
|
||||||
// unguarded as before.
|
|
||||||
//
|
|
||||||
// The blacklist itself comes from nix via `HIVE_RESERVED_NAMES`, so it
|
|
||||||
// stays a config change rather than a rebuild. An UNSET variable means
|
|
||||||
// this daemon was never told — which is not the same as "no name is
|
|
||||||
// reserved", and saying nothing there would be a check that reports
|
|
||||||
// clean because it could not run.
|
|
||||||
let raw = hive_types::reserved_names_raw();
|
|
||||||
let warnings = match raw.as_deref().map(hive_types::parse_reserved_names) {
|
|
||||||
None => {
|
|
||||||
tracing::error!(
|
|
||||||
var = hive_types::RESERVED_NAMES_ENV,
|
|
||||||
"request_init_config: reserved-name check could not run — variable not set"
|
|
||||||
);
|
|
||||||
vec![format!(
|
|
||||||
"the reserved-name check did not run: {} is unset, so {name:?} was accepted \
|
|
||||||
without being checked against the protocol literals",
|
|
||||||
hive_types::RESERVED_NAMES_ENV
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
Some(reserved) if hive_types::is_reserved_name(name, &reserved) => {
|
|
||||||
tracing::warn!(%agent, %name, "request_init_config: reserved name");
|
|
||||||
vec![format!(
|
|
||||||
"agent name {name:?} is a reserved protocol name — messages from this agent will \
|
|
||||||
be indistinguishable from hyperhive's own; this will become an error"
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
Some(_) => Vec::new(),
|
|
||||||
};
|
|
||||||
match submit_init_config(coord, name, Some(agent), description) {
|
|
||||||
Ok(_id) if warnings.is_empty() => Response::Ok,
|
|
||||||
Ok(_id) => Response::OkWarn { warnings },
|
|
||||||
Err(e) => Response::Err {
|
|
||||||
message: format!("{e:#}"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `RequestUpdateMetaInputs` — queue an `UpdateMetaInputs` approval
|
/// `RequestUpdateMetaInputs` — queue an `UpdateMetaInputs` approval
|
||||||
/// carrying the JSON-encoded input list in `commit_ref` (no git commit
|
/// carrying the JSON-encoded input list in `commit_ref` (no git commit
|
||||||
/// is involved; the field is the payload the approval handler decodes).
|
/// is involved; the field is the payload the approval handler decodes).
|
||||||
|
|
@ -214,58 +151,3 @@ pub(crate) async fn submit_merge_config_pr(
|
||||||
});
|
});
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue an `InitConfig` approval for a brand-new agent whose config repo
|
|
||||||
/// does not yet exist. Shared between the manager and agent sockets.
|
|
||||||
///
|
|
||||||
/// `parent`, when `Some`, is the agent that will own the new child once
|
|
||||||
/// the operator approves: it is stashed in the approval's `commit_ref`
|
|
||||||
/// field (unused for `InitConfig` otherwise — same pattern
|
|
||||||
/// `UpdateMetaInputs` uses to carry its inputs JSON) and consumed in
|
|
||||||
/// `run_approval_init_config` to write the `child -> parent` topology
|
|
||||||
/// edge. Callers pass the requesting agent, so the requester becomes the
|
|
||||||
/// new agent's parent (the root requesting a new agent → a top-level agent,
|
|
||||||
/// matching `topology::reconcile`'s default). `None` writes no explicit
|
|
||||||
/// edge (reconcile-default placement) — retained for that fallback.
|
|
||||||
pub(crate) fn submit_init_config(
|
|
||||||
coord: &Arc<Coordinator>,
|
|
||||||
name: &str,
|
|
||||||
parent: Option<&str>,
|
|
||||||
description: Option<String>,
|
|
||||||
) -> anyhow::Result<i64> {
|
|
||||||
let agent = hive_types::Ident::parse(name)
|
|
||||||
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
|
|
||||||
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(&agent);
|
|
||||||
if proposed_dir.join(".git").exists() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"proposed config repo for '{name}' already exists at {} - \
|
|
||||||
nothing to init; config changes go through a forge PR on \
|
|
||||||
agent-configs/{name}",
|
|
||||||
proposed_dir.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let id = coord
|
|
||||||
.approvals
|
|
||||||
.submit_kind(
|
|
||||||
name,
|
|
||||||
hive_sh4re::approvals::ApprovalKind::InitConfig,
|
|
||||||
parent.unwrap_or(""),
|
|
||||||
description.as_deref(),
|
|
||||||
// `parent` is the requesting agent (becomes the new child's
|
|
||||||
// parent); it's also the submitter the approval events route
|
|
||||||
// back to. No declared parent = operator-initiated path.
|
|
||||||
parent.unwrap_or("operator"),
|
|
||||||
None, // no sha for InitConfig
|
|
||||||
)
|
|
||||||
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
|
|
||||||
tracing::info!(%id, %name, "init_config approval queued");
|
|
||||||
coord.emit_approval_added(crate::coordinator::ApprovalAdded {
|
|
||||||
id,
|
|
||||||
agent: name,
|
|
||||||
approval_kind: "init_config",
|
|
||||||
sha_short: None,
|
|
||||||
description,
|
|
||||||
pr_number: None,
|
|
||||||
});
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ pub(crate) use config_approvals::submit_merge_config_pr;
|
||||||
pub(crate) use schedules::filter_ghost_schedule_targets;
|
pub(crate) use schedules::filter_ghost_schedule_targets;
|
||||||
pub use schedules::schedule_to_wire_public;
|
pub use schedules::schedule_to_wire_public;
|
||||||
|
|
||||||
use config_approvals::{handle_request_init_config, handle_request_update_meta_inputs};
|
use config_approvals::handle_request_update_meta_inputs;
|
||||||
use lifecycle_handlers::{
|
use lifecycle_handlers::{
|
||||||
handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update,
|
handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update,
|
||||||
};
|
};
|
||||||
|
|
@ -560,9 +560,6 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Respo
|
||||||
Request::Kill { name } => handle_kill(coord, agent, name).await,
|
Request::Kill { name } => handle_kill(coord, agent, name).await,
|
||||||
Request::Update { name } => handle_update(coord, agent, name),
|
Request::Update { name } => handle_update(coord, agent, name),
|
||||||
Request::ListDescendants => handle_list_descendants(coord, agent).await,
|
Request::ListDescendants => handle_list_descendants(coord, agent).await,
|
||||||
Request::RequestInitConfig { name, description } => {
|
|
||||||
handle_request_init_config(coord, agent, name, description.clone())
|
|
||||||
}
|
|
||||||
// Agent-state queries: own subtree is free; other agents + the
|
// Agent-state queries: own subtree is free; other agents + the
|
||||||
// hive-wide `"*"` sweep require `QueryAgentState`.
|
// hive-wide `"*"` sweep require `QueryAgentState`.
|
||||||
Request::GetLooseEnds { agent: target } => {
|
Request::GetLooseEnds { agent: target } => {
|
||||||
|
|
@ -695,46 +692,6 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option<Response> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Topology guard for `request_init_config`, which may legitimately target a
|
|
||||||
/// child that does not exist *yet* (seeding a brand-new sub-agent's config
|
|
||||||
/// repo). The caller may act on a
|
|
||||||
/// `target` that is EITHER already in its subtree (re-init / config
|
|
||||||
/// update of an agent it owns) OR brand-new (absent from the topology
|
|
||||||
/// tree — the requester becomes its parent). A name that already
|
|
||||||
/// exists outside the caller's subtree is refused so
|
|
||||||
/// one agent can't hijack another's sub-tree.
|
|
||||||
///
|
|
||||||
/// Also re-runs the agent-name format check (a traversal / malformed name
|
|
||||||
/// could never be a descendant): a brand-new name now flows straight to
|
|
||||||
/// `submit_init_config`, which builds filesystem paths from it, so validate
|
|
||||||
/// before that.
|
|
||||||
fn require_new_child(agent: &str, target: &str, action: &str) -> Option<Response> {
|
|
||||||
if let Err(reason) = hive_types::Ident::parse(target) {
|
|
||||||
return Some(Response::Err {
|
|
||||||
message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// brand-new name (absent from topology) — requester becomes the parent on
|
|
||||||
// approval; allowed for any caller.
|
|
||||||
if !crate::topology::read().contains_key(target) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// existing agent — allowed only if it's in the caller's subtree
|
|
||||||
// (re-init / config update of an agent the caller owns; the root owns
|
|
||||||
// every existing agent). Refuses an agent outside the caller's subtree
|
|
||||||
// so one agent can't hijack another's config.
|
|
||||||
if crate::topology::is_descendant_of(target, agent) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Response::Err {
|
|
||||||
message: format!(
|
|
||||||
"agent `{agent}` cannot {action} `{target}`: it already exists \
|
|
||||||
outside its subtree in the topology tree"
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `GetLooseEnds` — read the target's loose ends. `None` / own / a subtree
|
/// `GetLooseEnds` — read the target's loose ends. `None` / own / a subtree
|
||||||
/// descendant resolve freely (a parent sees its subtree, the root sees all);
|
/// descendant resolve freely (a parent sees its subtree, the root sees all);
|
||||||
/// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep
|
/// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Approval queue. Requests are submitted by the manager (`RequestInitConfig`
|
//! Approval queue. Requests are submitted by the manager
|
||||||
//! / `RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or
|
//! (`RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or
|
||||||
//! the operator (`Spawn`); the user approves/denies via the host admin CLI;
|
//! the operator (`Spawn`); the user approves/denies via the host admin CLI;
|
||||||
//! on approval the host runs the corresponding action.
|
//! on approval the host runs the corresponding action.
|
||||||
|
|
||||||
|
|
@ -74,8 +74,7 @@ impl Approvals {
|
||||||
/// Insert a new pending approval row. `fetched_sha` may be supplied
|
/// Insert a new pending approval row. `fetched_sha` may be supplied
|
||||||
/// when the sha is already known at submission time (e.g. `MergeConfigPr`
|
/// when the sha is already known at submission time (e.g. `MergeConfigPr`
|
||||||
/// fetches the PR head before inserting), making the insert + sha-set
|
/// fetches the PR head before inserting), making the insert + sha-set
|
||||||
/// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn` /
|
/// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn`).
|
||||||
/// `InitConfig`).
|
|
||||||
pub fn submit_kind(
|
pub fn submit_kind(
|
||||||
&self,
|
&self,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
|
|
@ -380,7 +379,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
||||||
let kind: String = row.get(2)?;
|
let kind: String = row.get(2)?;
|
||||||
let kind = match kind.as_str() {
|
let kind = match kind.as_str() {
|
||||||
"spawn" => ApprovalKind::Spawn,
|
"spawn" => ApprovalKind::Spawn,
|
||||||
"init_config" => ApprovalKind::InitConfig,
|
|
||||||
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
||||||
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
||||||
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
||||||
|
|
@ -434,7 +432,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
||||||
fn kind_from_str(s: &str) -> Result<ApprovalKind> {
|
fn kind_from_str(s: &str) -> Result<ApprovalKind> {
|
||||||
Ok(match s {
|
Ok(match s {
|
||||||
"spawn" => ApprovalKind::Spawn,
|
"spawn" => ApprovalKind::Spawn,
|
||||||
"init_config" => ApprovalKind::InitConfig,
|
|
||||||
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
|
||||||
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
"schedule_prompt" => ApprovalKind::SchedulePrompt,
|
||||||
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
"merge_config_pr" => ApprovalKind::MergeConfigPr,
|
||||||
|
|
@ -454,31 +451,6 @@ mod tests {
|
||||||
(dir, path, db)
|
(dir, path, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn init_config_approval_round_trips() {
|
|
||||||
// Regression test: an `init_config` row used to fail
|
|
||||||
// deserialization (row_to_approval matched only apply_commit +
|
|
||||||
// spawn), erroring out the whole `pending()` query — every
|
|
||||||
// approval then vanished from the dashboard.
|
|
||||||
let (_dir, _path, db) = open_temp();
|
|
||||||
let id = db
|
|
||||||
.submit_kind(
|
|
||||||
"bitburner",
|
|
||||||
ApprovalKind::InitConfig,
|
|
||||||
"",
|
|
||||||
Some("scaffold"),
|
|
||||||
"bitburner",
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.expect("submit init_config");
|
|
||||||
let pending = db
|
|
||||||
.pending()
|
|
||||||
.expect("pending() must not error on an init_config row");
|
|
||||||
assert_eq!(pending.len(), 1);
|
|
||||||
assert_eq!(pending[0].id, id);
|
|
||||||
assert!(matches!(pending[0].kind, ApprovalKind::InitConfig));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_kinds_all_listed() {
|
fn mixed_kinds_all_listed() {
|
||||||
let (_dir, _path, db) = open_temp();
|
let (_dir, _path, db) = open_temp();
|
||||||
|
|
@ -493,7 +465,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.submit_kind("b", ApprovalKind::Spawn, "", None, "b", None)
|
db.submit_kind("b", ApprovalKind::Spawn, "", None, "b", None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.submit_kind("c", ApprovalKind::InitConfig, "", None, "c", None)
|
db.submit_kind("c", ApprovalKind::UpdateMetaInputs, "[]", None, "c", None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let pending = db.pending().expect("pending");
|
let pending = db.pending().expect("pending");
|
||||||
assert_eq!(pending.len(), 3, "all three kinds must be visible");
|
assert_eq!(pending.len(), 3, "all three kinds must be visible");
|
||||||
|
|
|
||||||
|
|
@ -154,13 +154,6 @@ pub enum Request {
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---- privileged (manager socket only for now) ---------------------------
|
// ---- privileged (manager socket only for now) ---------------------------
|
||||||
/// *(privileged)* Initialise a brand-new agent's proposed config repo
|
|
||||||
/// and queue an approval for the operator to review.
|
|
||||||
RequestInitConfig {
|
|
||||||
name: String,
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
},
|
|
||||||
/// *(privileged)* Stop a sub-agent (graceful).
|
/// *(privileged)* Stop a sub-agent (graceful).
|
||||||
Kill { name: String },
|
Kill { name: String },
|
||||||
/// *(privileged)* Start a previously-stopped sub-agent container.
|
/// *(privileged)* Start a previously-stopped sub-agent container.
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,6 @@ pub enum ApprovalKind {
|
||||||
/// Create + start a new sub-agent container with the given name
|
/// Create + start a new sub-agent container with the given name
|
||||||
/// (under the default `agent.nix` template).
|
/// (under the default `agent.nix` template).
|
||||||
Spawn,
|
Spawn,
|
||||||
/// Create an agent's config repo and seed it from the default
|
|
||||||
/// template (step 1 of the two-step spawn flow). Creating it is the
|
|
||||||
/// whole of this step — tailoring what the template seeded is not a
|
|
||||||
/// separate mechanism, it's a `MergeConfigPr` like every later
|
|
||||||
/// change.
|
|
||||||
InitConfig,
|
|
||||||
/// Run `nix flake update [inputs...]` on the meta flake and commit
|
/// Run `nix flake update [inputs...]` on the meta flake and commit
|
||||||
/// the resulting lock changes.
|
/// the resulting lock changes.
|
||||||
UpdateMetaInputs,
|
UpdateMetaInputs,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub enum ToolGroup {
|
||||||
Inbox,
|
Inbox,
|
||||||
/// `kill`, `start`, `restart`, `update` - *(privileged)*
|
/// `kill`, `start`, `restart`, `update` - *(privileged)*
|
||||||
Lifecycle,
|
Lifecycle,
|
||||||
/// `request_init_config`, `request_update_meta_inputs` - *(privileged)*
|
/// `request_update_meta_inputs` - *(privileged)*
|
||||||
Approvals,
|
Approvals,
|
||||||
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
|
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
|
||||||
/// `edit_schedule`, `list_schedules` - *(privileged)*
|
/// `edit_schedule`, `list_schedules` - *(privileged)*
|
||||||
|
|
@ -60,7 +60,7 @@ impl ToolGroup {
|
||||||
Self::Meta => &["get_agent_meta"],
|
Self::Meta => &["get_agent_meta"],
|
||||||
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
|
||||||
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"],
|
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"],
|
||||||
Self::Approvals => &["request_init_config", "request_update_meta_inputs"],
|
Self::Approvals => &["request_update_meta_inputs"],
|
||||||
Self::Scheduling => &[
|
Self::Scheduling => &[
|
||||||
"request_schedule_prompt",
|
"request_schedule_prompt",
|
||||||
"fire_schedule_now",
|
"fire_schedule_now",
|
||||||
|
|
@ -167,7 +167,7 @@ impl ToolGroup {
|
||||||
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
|
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
|
||||||
}
|
}
|
||||||
Self::Approvals => {
|
Self::Approvals => {
|
||||||
"request_init_config, request_update_meta_inputs — config change flow (privileged)"
|
"request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)"
|
||||||
}
|
}
|
||||||
Self::Scheduling => {
|
Self::Scheduling => {
|
||||||
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
|
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ One module per subcommand family; `main.rs` is just the clap parse +
|
||||||
dispatch:
|
dispatch:
|
||||||
|
|
||||||
- **`agents.rs`** — container lifecycle (start/stop/create/kill/rebuild/restart/…).
|
- **`agents.rs`** — container lifecycle (start/stop/create/kill/rebuild/restart/…).
|
||||||
- **`approvals.rs`** — the config/init-config/meta-input approval queue.
|
- **`approvals.rs`** — the config/spawn/meta-input approval queue.
|
||||||
- **`dag_progress.rs`** — rebuild-queue progress rendering.
|
- **`dag_progress.rs`** — rebuild-queue progress rendering.
|
||||||
- **`power.rs`** — restart/start/stop at the container level.
|
- **`power.rs`** — restart/start/stop at the container level.
|
||||||
- **`choom.rs`** — drop into an interactive claude session in a
|
- **`choom.rs`** — drop into an interactive claude session in a
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@
|
||||||
# (`HIVE_RESERVED_NAMES`), so keeping it current is a config change rather than
|
# (`HIVE_RESERVED_NAMES`), so keeping it current is a config change rather than
|
||||||
# a rebuild of a binary. Read by:
|
# a rebuild of a binary. Read by:
|
||||||
#
|
#
|
||||||
# - `host-modules/hive-c0re/environment.nix` -> the env var, for the
|
# - `host-modules/swarm-controller.nix` -> the env var, for the
|
||||||
# `request_init_config` path every hive uses
|
# swarm-level `create_agent` path — the only name check that runs today
|
||||||
# - `host-modules/swarm-controller.nix` -> the same var, for the
|
# - `host-modules/hive-c0re/environment.nix` -> the same var. hive-c0re
|
||||||
# swarm-level `create_agent` path
|
# creates no agent, so nothing reads it there now; still handed over so a
|
||||||
|
# future host-side path is wired by construction, not by remembering to.
|
||||||
# - `host-modules/swarm-otel.nix` -> its `<owner>` assertion, so
|
# - `host-modules/swarm-otel.nix` -> its `<owner>` assertion, so
|
||||||
# a hive name and an agent name are checked against ONE list
|
# a hive name and an agent name are checked against ONE list
|
||||||
# - `nix/checks.nix` -> exported into `cargo test`,
|
# - `nix/checks.nix` -> exported into `cargo test`,
|
||||||
# which is what keeps the message layer's sentinels from drifting away
|
# which keeps the message layer's sentinels from drifting from this file
|
||||||
# from this file
|
|
||||||
#
|
#
|
||||||
# A plain nix file rather than a module option because two of those readers are
|
# A plain nix file rather than a module option because two of those readers are
|
||||||
# flake-level (`checks.nix`) and cannot see a NixOS option.
|
# flake-level (`checks.nix`) and cannot see a NixOS option.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue