Per mara (verified in forge.rs): agents have max_repo_creation=0, so every
internal-forge repo is core-created with branch protection on by default
(merge restricted to operators team + required operators-team approval via
apply_operator_branch_protection / the config-repo equivalent), and agents
(write collaborators, not admins) can't change it or self-merge. So it's
technically enforced there, not just convention — only external VCS (GitHub)
is unprotected. Corrects my prior over-correction.
Per mara: branch protection isn't a blanket enforced check, and it's not
set up for external VCS (GitHub). Reframe the bullet as the operator-merge
*convention* — technically enforced only on the core-managed config repos,
and explicitly NOT wired up for GitHub/external VCS (process + accepted
risk there, not a control).
The trust-model insert accidentally consumed the
'## State-file endpoint security model' heading, leaving that section
floating under the new one. Re-add the heading and collapse the extra
blank lines (argus review).
Add an 'Agent trust model' framing section at the top of docs/security.md
covering the conceptual model the existing mechanism-level sections serve:
- trust boundary = the container, not credential storage (agents have
passwordless sudo by default; isolating creds from the agent itself is
not a goal — cross-tenant leakage is)
- scoped per-agent forge/matrix tokens bound the blast radius
- threat model: prompt injection -> confused deputy (untrusted input is
the adversary; the agent + its tools are trusted)
- branch protection (agents push, operator merges) as the human-in-loop
checkpoint, incl. external VCS
- capability = accepted risk ('don't grant what you can't afford to lose')
- no auto-sandboxing of external tokens (operator-accepted scope)
Emerged from the 2026-06-24 security discussion. Updates the CLAUDE.md
pointer for findability.
Follow-up to the harness fix: any unit whose ExecStart is a generated
wrapper (a NixOS `script =` block or a writeShellScript) gets its journal
SyslogIdentifier derived from the wrapper's store-path basename — an
opaque, unstable name. Pin an explicit stable identifier on every unit
hyperhive declares.
script/wrapper units (leaked store paths): forgejo-gpg-init,
hive-gateway-self-signed-cert, hive-tls-ca, hive-ci-prefetch,
hyperhive-isolated-dns, tea-login, forge-avatar-sync,
matrix-avatar-sync, hive-dashboard-links.
direct-binary units (basename already sane; explicit for tidiness +
future-proofing): hive-c0re, hive-priv, hive-matrix-daemon,
hive-bash-daemon, weston.
Only changes the displayed line identifier; journalctl -u <unit>
filtering (keys on _SYSTEMD_UNIT) is unaffected.
notify_manager (always-root) has been replaced by notify_submitter
(routes to the approval submitter, with root fallback for legacy rows).
Update three locations:
- Step 6 of end-to-end flow: "root agent's inbox" →
"submitting agent's inbox"; explain notify_submitter + submitter column.
- InitConfig paragraph: remove limitation note; "to the root agent's inbox
(current limitation...)" → "to the submitting agent's inbox via notify_submitter".
- Section header + prose: "Helper events to the root agent" →
"Helper events to the submitting agent"; rewrite lead paragraph around
notify_submitter semantics (approvals.submitter column, legacy fallback).
Remove three bare #NNN tracker-tag references in docs/approvals.md
(hive-rules: no #NNN in code/comments/docs):
- Step 6 of end-to-end flow: "tracked in #1953" → "a known limitation
under active development"
- InitConfig paragraph: same rewrite
- Helper events section: same rewrite
Also add a code pointer for `can_manage_top_level_agents`: the role is
defined as ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS in hive-c0re/src/topology.rs,
now noted inline so reviewers can verify the name is real.
The last docs/ piece of the manager-cleanup. The manager is no longer a
structural role — root-ness is purely topological. Reframe:
- title 'Approvals + manager + helper events' -> 'Approvals + helper events'
- section headers: 'Manager view of applied'/'Manager policy'/'Manager
(ruth) is hive-c0re-managed'/'Helper events to the manager' ->
root-agent / root-bootstrap-container equivalents
- body prose: 'the manager (ruth)' -> 'the root agent' (or 'the submitter'
in the approval-flow steps)
- authority semantics: 'manager-only' -> approvals are submitted by an
agent with the approvals tool group, for its direct children
- dropped the stale 'the manager refuses to destroy itself' line (the
bootstrap container is now destroyable + transient; recreated on startup)
Kept the genuine code/protocol identifiers (nixosConfigurations.manager,
manager_server, role:manager prompt block, notify_manager, the
/run/hyperhive/manager/ socket path) — renaming those would diverge from
the source (de-hardcoding is its own backend cleanup).
- Remove the two issue-number tags from Rust comments (tracker-tag lint).
- Correct the module doc: the durable download is keyed by the per-repo
run NUMBER + attempt segment (matches the code + persisted_logs doc),
not a global id — the earlier 'global id' wording was stale.
- Replace the fragile e.to_string().contains("out of range") branch with
a typed StreamerMiss enum (StepOutOfRange vs Unavailable), so a usage
error can never silently fall through to the persisted-log path if a
message string changes.
ci-log drove only the live run-view streamer (POST .../runs/<n>/jobs/<j>
with logCursors), which reads the live act_runner task record. Forgejo
prunes that record once a run completes, so the streamer 500s with
'task ... resource does not exist' on quick or older runs even though
the web UI still shows the logs — the reader (argus, atlas) then had to
ask the operator to relay the error.
Add a fallback: when the streamer errors or returns no lines, download
the persisted whole-job log via the same web route the run page's view-
raw-logs link uses, .../runs/<n>/jobs/<job>/attempt/<a>/logs, keyed by
the per-repo run number with the attempt segment. It survives the task
prune. Live and recent runs keep the rich per-step streamed view; only
the pruned case takes the flat fallback (where --step can't apply). New
--attempt flag (default 1) selects the run attempt for re-runs.
Verified against a real pruned run whose streamer 500'd: the fallback
prints the full persisted log; a completed short job ends cleanly at
'Job succeeded', confirming the route returns complete logs.
Per operator directive: a silent automatic force-push is a bug. The
config-repo merge path already never force-pushes (run_merge_config_pr
lands via ff_push_to_main, a non-force git push). So set the branch
protection's enable_force_push to false — main only ever advances by
fast-forward.
The legacy push_config mirror does force-push (it re-points the status
tags and rewinds main on a failed-build rollback); the protection now
rejects those non-ff updates, so the mirror runs best-effort until the
agent-opened PR-merge flow retires it. Docs + comments updated to match.
Address argus review on the config-repo PR surface: 6 issue-number tags
appeared in Rust doc comments in forge.rs; the tracker-tag lint matches
${'#'}NNN in code comments (markdown docs are exempt, doc comments are not).
Reword to prose, no behavior change.
Address iris review nit on #1948: the bullet above now reframes the
agent-configs entry as a config repo, so the dashboard-anchor bullet
should match. Pure wording, no behavior change.
Wires the per-agent config repo as the editable PR surface the #1838
merge handler (run_merge_config_pr) consumes, without yet retiring the
push_config force-mirror (that waits for #1838 P2 — the agent-opens-PR
MCP surface — so config changes keep working through the transition).
ensure_config_repo now, after creating agent-configs/<name>:
- adds the agent as a WRITE collaborator (can push config-change branches
+ open PRs);
- branch-protects main core-only via apply_config_repo_branch_protection:
push + merge whitelists are core-only (only hive-c0re lands on main, via
its verify-and-ff-push handler), operator-team approval required, the
agent can't push main directly or self-merge.
The protection sets enable_force_push=true as a TRANSITIONAL allowance so
push_config's applied->main force-mirror keeps working until P2 retires it
(a protected branch otherwise rejects force-push). Forgejo's force-push
allowlist is deploy-keys-only — no per-user list — so this is a plain
enable_force_push toggle; only core is in the push-whitelist so only core
can force-push anyway. At P2, flip it to false and keep core in the
push-whitelist so ff_push_to_main still lands.
Agent code repos (AGENTS_ORG) are intentionally untouched — their merge
flow has no auto-merge handler yet, so switching them to core-only-merge
would strand those PRs.
All steps idempotent (runs on every spawn + startup sweep). Updated the
CONFIG_ORG doc comment + docs/forge.md to drop the "mirror-only" framing.
coordinator.md: GracefulStop entry referenced the old bare /kill/<agent>
path with the lax graceful=1 bool — update to /api/kill/<agent>?graceful=true.
approvals.md: the auto-update badge description referenced /rebuild/<name>
(bare, no /api/ prefix) — update to /api/rebuild/<name>.