diff --git a/changelog.d/added-securing-your-hive.md b/changelog.d/added-securing-your-hive.md new file mode 100644 index 0000000..045c3f4 --- /dev/null +++ b/changelog.d/added-securing-your-hive.md @@ -0,0 +1 @@ +- Publish Hive's first-time operator security guide under Security, synced from `hivecommons/hive@v5`, and keep links to it from the Introduction and Security model pages on the documentation site. diff --git a/docs/content/hive/readme.md b/docs/content/hive/readme.md index 2eb8c7d..023f158 100644 --- a/docs/content/hive/readme.md +++ b/docs/content/hive/readme.md @@ -36,7 +36,14 @@ Start with [Zero to Automation: Getting Started with Hive](/docs/hive/getting-st - [Architecture](/docs/hive/architecture) - [Getting started](/docs/hive/getting-started) - [Operator reference](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md) +- [Maintainer commands](https://github.com/hivecommons/hive/blob/v5/src/docs/maintainer-commands.md) — slash commands for un-parking + issues, requesting help, confirming fixes, reopening issues, and using label + or assignment helpers. +- [Release channels](/docs/hive/release-channels) — what `stable`, `candidate`, and `edge` each mean and what to expect from them. - [Security model](/docs/hive/security-model) +- [Data collection and telemetry](https://github.com/hivecommons/hive/blob/v5/src/docs/telemetry.md) +- [Securing your hive: a first-time operator's guide](/docs/hive/securing-your-hive) +- [Community and support](https://github.com/hivecommons/hive/blob/v5/src/docs/community.md) - [Documentation map](/docs/hive/documentation-map) Current docs target branch `v5`; use the [documentation map](/docs/hive/documentation-map) for v2 → v4 and v4 → v5 migration pointers. diff --git a/docs/content/hive/securing-your-hive.md b/docs/content/hive/securing-your-hive.md new file mode 100644 index 0000000..15cc903 --- /dev/null +++ b/docs/content/hive/securing-your-hive.md @@ -0,0 +1,465 @@ +> **Synced from Hive.** This page is pulled from [hivecommons/hive@v5](https://github.com/hivecommons/hive/blob/v5/src/docs/securing-your-hive.md) during the docs build. Edit the canonical source in the Hive repository. + +# Securing your hive: a first-time operator's guide + +You've just connected a hive to a repository. Before you pick a level and walk +away, you need a plain answer to one question: **who can make Hive do what in +my repo?** The facts are correct and complete elsewhere — in +[security-model.md](/docs/hive/security-model), the [ACMM policy +matrix](/docs/hive/acmm-policy-matrix), [security-threat-model.md](/docs/hive/security-threat-model), +[agent-configuration.md](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled), +and [contributor-trust-and-roles.md](https://github.com/hivecommons/hive/blob/v5/src/docs/contributor-trust-and-roles.md) — but +they are mechanism-oriented, and a first deployment shouldn't require reading +all five before you understand what you just turned on. + +This guide is decision-oriented. It doesn't repeat the tables in those pages; +it links to them and explains what the settings *mean* for a stranger filing +an issue against your repo tomorrow morning. + +## The 60-second version + +Three settings decide who can make Hive act, and how far: + +1. **ACMM level** (`acmm_level`, 1–6). This is the big dial. It decides which + agents run, and whether each one can only observe, file issues, open + held pull requests, or open-and-merge pull requests. See the [ACMM policy + matrix](/docs/hive/acmm-policy-matrix) for the full per-level, per-agent table. +2. **Reporter trust** (`project.issue_filter.reporter_trust`, off by + default). Decides whether Hive tells *maintainer* issues from *stranger* + issues apart — not just by label, but by who GitHub says filed them. See + [agent-configuration.md § Reporter + trust](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled). +3. **GitHub App install scope / repo allowlist.** Whichever repositories you + installed the Forge App on (or listed in `project.repos`) are the only + ones a hive can touch at all, enforced twice: once at the network proxy and + once as a prompt-level reminder. See [security-model.md § Layer + 5](/docs/hive/security-model#layer-5--github-blast-radius-controls). + +Everything below is these three settings interacting with a fourth fact you +don't configure: GitHub's own `author_association` on every issue and PR +(`OWNER`, `MEMBER`, `COLLABORATOR`, `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, +`FIRST_TIMER`, `NONE`) — see the [glossary](#glossary) below. + +## Pick a starting posture + +Every posture below assumes the repo allowlist only covers repos you actually +want Hive touching — that part doesn't change with level or reporter trust. + +### Cautious — L4/L5, reporter trust off + +**A stranger files an issue:** an agent may open an issue about it (at L4, +only quality/sec-check/ci-maintainer may open PRs; at L5, every agent may). +Either way, any resulting pull request is held: at L5 the level gate labels +**every** agent PR `hold`, no exceptions. At L4, only the agents whose mode is +`holdgated` (quality, ci-maintainer, sec-check) can open a PR at all, and +those are held the same way; the rest stay `measured` (issues only) or +`advisory`. Don't describe L4 as "L5 but smaller" — it's a genuinely mixed +roster, not a uniformly held one. See the [L3–L5 agent/mode +tables](/docs/hive/acmm-policy-matrix#l4--security-aware-adaptive-7-agents). + +**A known contributor (`COLLABORATOR`/`MEMBER`) files an issue:** same +treatment — reporter trust is off, so Hive doesn't look at who asked, only at +what labels and gates apply. The PR is still held the same way. + +**Who merges:** a human, always. Merge permission "simply is not granted below +L6" — the token tier and proxy rules refuse it regardless of level-hold +labels (see [security-model.md § Layer +5](/docs/hive/security-model#layer-5--github-blast-radius-controls)). + +### Trusted team — L6, reporter trust on, trusting OWNER/MEMBER/COLLABORATOR + +This is the reporter-trust default set — you don't have to list anything to +get it: + +```yaml +project: + issue_filter: + reporter_trust: + enabled: true +``` + +**Anyone outside the trusted set (`CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, +`FIRST_TIMER`, `NONE`) files an issue:** not actionable until someone adds +`triage/accepted` (the default `untrusted_require_labels`). +Once triaged, an agent may work it — but the resulting PR still gets `hold` +at every level, L6 included, because the reporter-trust merge-side check +re-evaluates who the rationale traces to; triaging the issue does not +un-hold the PR. See [agent-configuration.md § +Admission/Merge](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled). + +**A team member (`MEMBER`/`COLLABORATOR`/`OWNER`) files an issue:** admitted +and worked exactly as before reporter trust existed — no triage label needed, +and at L6 the resulting PR merges on green CI with no level hold (unless some +other hold applies). + +**Who merges:** agents, autonomously, for team-filed work. A human, always, +for anything traceable to an untrusted reporter. + +### Open community — L6, reporter trust on, also trusting CONTRIBUTOR + +```yaml +project: + issue_filter: + reporter_trust: + enabled: true + trusted_associations: [OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR] +``` + +**A `CONTRIBUTOR` (someone with at least one merged commit, ever) files an +issue:** admitted like a team member's — no triage label required, and a +resulting PR can merge unattended on green CI. + +**Anyone else** (`FIRST_TIME_CONTRIBUTOR`, `FIRST_TIMER`, `NONE`, or an +association GitHub didn't report): requires `triage/accepted` for admission, +and any resulting PR is still held for a human, exactly as in the trusted-team +posture above. + +**Who merges:** agents, for anyone with standing history in the repo. A human +for first-time strangers, always — this posture widens *whose* work gets +autonomy, not *whether* an unvetted stranger's work can merge unattended. + +## Who gets worked, who gets merged: the reporter-trust matrix + +The trusted-team posture above, as two tables. Both assume reporter trust is +on with the default boxes (`OWNER`, `MEMBER`, `COLLABORATOR` checked; +everything else unchecked), the triage label is `triage/accepted`, the repo's +reporter-trust hold has not been switched off on the Repos tab, and the hive +is at L6. + +### The short version + +Two questions decide what happens to an issue: was it filed by someone +trusted, and does it carry `triage/accepted`? + +| | **No `triage/accepted`** | **Has `triage/accepted`** | +|---|---|---| +| **Filed by a trusted person** | ✅ Worked. Hive's PR merges on its own on green CI. | ✅ Worked. Hive's PR merges on its own on green CI (the label changes nothing). | +| **Filed by anyone else** | ⛔ Not worked. | ✅ Worked. ✋ Hive's PR is held until a person removes `hold`. | + +"Trusted" means the reporter's GitHub association is one of the checked boxes, +or their login is under **Always-trusted logins**. + +### Who counts as what + +GitHub, not Hive, decides each issue author's association. + +| Filed by | How someone ends up here | Default box | Worked without `triage/accepted`? | Worked with `triage/accepted`? | Hive's PR merges on its own at L6? | +|---|---|---|---|---|---| +| `OWNER` | Owns the repo or org | ✅ | Yes | Yes | Yes, on green CI | +| `MEMBER` | Is a member of the org that owns the repo | ✅ | Yes | Yes | Yes, on green CI | +| `COLLABORATOR` | Was given access to the repo (invited) | ✅ | Yes | Yes | Yes, on green CI | +| Always-trusted login | You added their login on the Labels tab | n/a | Yes | Yes | Yes, on green CI | +| `CONTRIBUTOR` | **Automatic:** has previously committed to this repo (one typo fix is enough) | ☐ | No | Yes | **No.** Held until a person removes `hold` | +| `FIRST_TIME_CONTRIBUTOR` | **Automatic:** has commits elsewhere on GitHub, none here | ☐ | No | Yes | **No.** Held until a person removes `hold` | +| `FIRST_TIMER` | **Automatic:** has never committed anywhere on GitHub | ☐ | No | Yes | **No.** Held until a person removes `hold` | +| `NONE` | No relationship to the repo | ☐ | No | Yes | **No.** Held until a person removes `hold` | + +### What changes automatically, and what doesn't + +`FIRST_TIMER`, `FIRST_TIME_CONTRIBUTOR` and `CONTRIBUTOR` come from commit +history alone, and GitHub changes them by itself: a reporter's first commit to +this repo makes them `CONTRIBUTOR`, whichever of the other two they were +before. (`NONE` just means no relationship to the repo.) None of these rows is +checked by default, so **commit history alone never makes anyone trusted**. +The open-community posture above checks `CONTRIBUTOR`. That's the one setting +where a single merged PR does make someone trusted. + +The checked rows change only when someone is given repo access or org +membership. That usually means a person sending an invite. If your org or repo +grants either automatically (a team sync, an onboarding bot, or a workflow +that invites contributors after their first merge), whoever it grants becomes +trusted too. In that case, uncheck the box that automation feeds and trust +people by login instead. + +**Always-trusted logins** trusts one person by name without giving them any +repo access, which inviting them as a collaborator would. + +### Things that catch people out + +- **`MEMBER` means every member of the owning org,** including people with no + access to this repo. In a large org that's a lot of people. If it's too + broad, uncheck `MEMBER` and list the people you trust by login. +- **`COLLABORATOR` doesn't say how much access someone has.** GitHub reports + the association without the permission level, so Hive can't tell a + read-only collaborator from an admin. Checking the box trusts everyone you + have added as a collaborator. +- **Hive checks that `triage/accepted` is present, not who added it.** Anyone + with Triage access or higher on the repo can add it. Triage only lets the + work start; the hold is what stops the merge. +- **The hold depends on who filed the issue, not on who did the work.** It + doesn't matter which agent or contributor wrote the PR. If the PR is linked + to any issue from an untrusted reporter (`Closes #N`, `Refs #N`, or the + issues the agent declared when it asked Hive to open the PR), it's held. +- **The hold only covers PRs Hive opens.** A PR a person opens by hand goes + through the repo's normal branch protection and review rules. +- **Issues filed by bots or by Hive itself skip reporter trust.** A different + safeguard handles them on the PR side: the `#5117` self-authorization hold, + which is off by default at L6. +- **Trusted doesn't mean unstoppable.** `require_labels`, existing holds, and + a repo's `auto_merge` setting still apply. "Worked" and "merges on its own" + in the tables only mean reporter trust isn't what stops it. + +## Walk-through: why did Hive merge this without my `/lgtm`? + +Real case, [#9758](https://github.com/hivecommons/hive/issues/9758) → +[#9762](https://github.com/hivecommons/hive/pull/9762). A contributor +(`author_association: CONTRIBUTOR`) filed #9758, a small, well-scoped +documentation/text bug (wrong kernel module names in a remediation message). +The hive's scanner/quality lane picked it up, opened #9762 fixing it, and the +PR merged about 79 minutes after it was requested — with **no** prow `/lgtm` +or `/approve`, and no `hold` label at any point. + +What allowed each step, in order: + +1. **Admission.** The reporter's association was `CONTRIBUTOR`. Whatever this + repo's reporter-trust configuration was at the time, #9758 was admitted: + either reporter trust was off (association never gates admission), or it + was on with `CONTRIBUTOR` explicitly added to `trusted_associations` — the + open-community posture above. The one configuration that would have + required a maintainer to add `triage/accepted` first is reporter trust on + *with the trusted-team default* (`OWNER`/`MEMBER`/`COLLABORATOR` only, + `CONTRIBUTOR` excluded) — that did not happen here, since #9758 was worked + with no triage label. +2. **No level-hold on the PR.** The repo runs at an ACMM level where this + agent's PRs are not held-gated, so #9762 never carried the level-hold + `hold` label the L3–L5 packs would apply. +3. **No prow gate.** Prow bot activity on the PR (`dco-signoff: yes`, + `size/XS`) is CI plumbing, not a merge gate for Hive's own merges — Prow's + `tide` merge queue requires `lgtm`+`approved` labels that only a *human + reviewer* can apply, and the Forge App can never review its own PR. Hive's + self-merge sweep exists specifically because of that: it merges the App's + own green PRs directly over the REST API, bypassing tide entirely. See + [operator-reference.md § App self-merge + sweep](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#app-self-merge-sweep-auto_merge). +4. **Green CI.** The self-merge sweep only merges PRs that are clean, + non-draft, and green — #9762's CI passed, so the sweep merged it on its + next pass. + +The behavior was correct for that repo's configuration, but as the issue that +prompted this guide notes, confirming it took a maintainer several docs plus +the GitHub timeline. That's the gap this page closes. + +## Q&A for first deployments + +**Can an untrusted reporter get code merged?** +No, not unattended. Their issue waits for `triage/accepted` (or your +configured label) if reporter trust is on. Once admitted (or if reporter +trust is off entirely), a resulting PR is still held for a human at **every** +ACMM level, including L6 — reporter trust's merge-side check runs +independently of the level gate. "Untrusted" means *outside the trusted +associations/logins*, not "no merge history": `OWNER`, `MEMBER`, and +`COLLABORATOR` are trusted by default even for an account with zero merged +commits, because GitHub itself vouches for the relationship (org membership +or repo collaborator access), which a merge count does not measure. + +**Does prow `/lgtm` / `/approve` / tide gate Hive's own merges?** +No. Tide's `lgtm`+`approved` requirement governs the **human queue** path +(`governor.labels.automerge`, default label `lgtm` — a merger/owner applies +it and Hive squash-merges once CI is green). It structurally cannot gate a +PR the Forge App itself opened, because the App can't review its own work; +that's exactly why the separate self-merge sweep exists, and it bypasses +tide by merging directly over the REST API. See +[contributor-trust-and-roles.md](https://github.com/hivecommons/hive/blob/v5/src/docs/contributor-trust-and-roles.md) and +[operator-reference.md § App self-merge +sweep](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#app-self-merge-sweep-auto_merge). + +**What does `CONTRIBUTOR` actually mean? Why is it off by default?** +GitHub reports `CONTRIBUTOR` when the account has at least one commit merged +into the repository's default branch, at any point in its history — one +merged typo fix qualifies forever. It's excluded from the default trusted set +deliberately: a single past contribution doesn't make someone a maintainer, +so `DefaultTrustedAssociations` is `OWNER`, `MEMBER`, `COLLABORATOR` only +(`src/pkg/config/reporter_trust.go`). Add `CONTRIBUTOR` explicitly (the +open-community posture above) if your project wants to extend +unattended-merge trust to anyone with contribution history. + +**Is an org `MEMBER` or `COLLABORATOR` trusted even with no merges?** +Yes. `ReporterTrustConfig.Trusted()` checks the reporter's +`author_association` (or an explicit login match) — it never looks at merge +or PR history. The association alone is enough. + +**What exactly does `triage/accepted` unlock, and what does it not unlock?** +It unlocks **admission**: an agent may now work the issue (open a PR about +it, comment, classify it) the same as any other actionable issue. It does +**not** unlock unattended merge. If the resulting PR's rationale traces back +to that untrusted-reporter issue, the reporter-trust merge-side check still +applies `hold` at every level — a human still has to remove that label. + +**How do I stop all auto-merges right now?** +Drop below L6 — merge permission is not granted below L6 at the token/proxy +level, so no config error or race can produce an unattended merge. For a +single in-flight item instead of the whole hive, apply the dashboard's +`hive-pause/` label to that issue or PR (see [Hive Labels and +Control Signals](https://github.com/hivecommons/hive/blob/v5/src/docs/labels-and-control-signals.md)); it is a manual hold +distinct from the level-hold `hold` label and from the provenance-only +`hive/` label. + +**What changes for existing hives when I turn reporter trust on?** +Nothing immediately for maintainer-filed issues — `OWNER`/`MEMBER`/ +`COLLABORATOR` issues flow exactly as before. Issues from anyone else stop +being actionable until triaged, and once reporter-trust is enabled its PR-side +hold defaults on too (`github.reporter_trust_hold` follows +`reporter_trust.enabled` unless you set it explicitly), so any PR tracing to +an untrusted-reporter issue starts getting held for review even at L6. It is +opt-in for exactly this reason — an existing hive that takes issues from the +public changes nothing until you flip the switch. + +**Which levels can open PRs, and which can merge?** +See the [ACMM policy matrix](/docs/hive/acmm-policy-matrix) for the authoritative, +per-agent table. In summary: L1–L2 agents never open PRs (advisory only); at +L3 only `quality` can (held); at L4 `quality`, `ci-maintainer`, and +`sec-check` can (held); at L5 every non-paused holdgated agent can, and every +PR it opens is held — `reviewer` is `converse` (comments and reviews, never +opens a PR) and `adjudicator` is `issues+prs`; at L6 every non-paused full-mode +agent can open **and merge** on green CI, except `outreach` PRs, which stay +held at every level `outreach` exists (L6 only), and `reviewer`/`adjudicator`, +which never merge at any level. + +**How do I trust one specific outside person without trusting a whole +association?** +Add their login to `trusted_logins` — it's checked before association and +overrides it, so an external maintainer whose GitHub association reports as +`NONE` (no org membership, no collaborator grant) can still be treated as +trusted by name: + +```yaml +project: + issue_filter: + reporter_trust: + trusted_logins: [external-maintainer] +``` + +## Checklist before going to L6 + +- [ ] You've spent real weeks at L3–L5 and the PRs you've reviewed matched + your judgment consistently — see [Getting Started § Trust > + Level](/docs/hive/getting-started#trust--level-always). There's no calendar + requirement; the requirement is that you're not surprised. +- [ ] Your test suite is strong enough that green CI genuinely means "safe to + ship" — L6 has no human PR gate for non-outreach agent work, so tests + are the only backstop left. +- [ ] You've decided your reporter-trust posture (off, trusted-team, or + open-community above) *before* enabling L6, not after — an L6 hive with + reporter trust off will merge a total stranger's request the moment CI + is green. +- [ ] If you take public issues at all, reporter trust is on, with the + association set you actually intend (does this project want + `CONTRIBUTOR` trusted, or not?). +- [ ] You know how to pull the emergency brake: drop the level, or apply + `hive-pause/` to one item — see the Q&A above. +- [ ] `outreach`'s PRs are still held at every level including L6 by design; + don't expect that to change without a config option, because there + isn't one. + +## Glossary + +- **hive** — one running instance of Hive: the governor, agents, dashboard, + and state for one deployment. Every hive is a **spoke**. See + [architecture.md § 8](/docs/hive/architecture#8-hub--spoke). +- **hub** — the one hosted instance (`hive.hivecommons.dev`, or your own + self-hosted hub) that registers, provisions, and observes many spokes. Same + container image as a spoke; `HIVE_MODE=hub` selects the role. See + [architecture.md § 8](/docs/hive/architecture#8-hub--spoke). +- **spoke** — a hive, from the hub's point of view: it pushes heartbeats and + receives callbacks (config, banners, branch switches, authorized users). +- **ACMM, ACMM level (L1–L6)** — the maturity model that maps a single dial to + a per-agent roster and set of policy modes, from advisory-only (L1–L2) + through fully autonomous merge-on-green (L6). See the [ACMM policy + matrix](/docs/hive/acmm-policy-matrix). +- **policy mode: advisory, measured, holdgated, full** — the four per-agent + capability tiers. Advisory observes only; measured can file issues; holdgated + can open PRs but every PR gets `hold`; full can open and merge PRs on green + CI. See [acmm-policy-matrix.md § Policy + Modes](/docs/hive/acmm-policy-matrix#policy-modes). +- **agent / lane** — a configured AI worker (`scanner`, `quality`, `sec-check`, + …) with its own mode, cadence, model, and kick template. "Lane" is the same + concept viewed as a stream of work. See + [agent-configuration.md](/docs/hive/agent-configuration). +- **reporter trust** — the opt-in gate (`project.issue_filter.reporter_trust`) + that admits or holds work based on *who filed the issue*, using GitHub's + `author_association`, separately from any label. See + [agent-configuration.md § Reporter + trust](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled). +- **GitHub author association** — GitHub's own classification of an issue or + PR author's relationship to the repo: `OWNER`, `MEMBER`, `COLLABORATOR`, + `CONTRIBUTOR` (at least one merged commit, ever), `FIRST_TIME_CONTRIBUTOR`, + `FIRST_TIMER`, `NONE`. Hive treats an unknown/missing association as + untrusted. +- **always-trusted logins** — `project.issue_filter.reporter_trust.trusted_logins`, + an explicit login list trusted regardless of association — for an external + maintainer GitHub doesn't otherwise vouch for. +- **triage label (`triage/accepted`)** — the default label + (`untrusted_require_labels`) that admits an untrusted reporter's issue for + agent work. Hive checks that it is present, not who added it, so anyone with + Triage access or higher on the repo can admit an issue. Does not by itself + remove a PR-side reporter-trust hold. +- **`hold`** — the literal label multiple gates apply (level gate, + reporter-trust hold, `#5117` self-authorization hold, SHA-hold, holdguard). + Any label containing the substring `hold` is treated as a hard hold by + enumeration and merge sweeps. See [Hive Labels and Control + Signals](https://github.com/hivecommons/hive/blob/v5/src/docs/labels-and-control-signals.md). +- **`hive-pause/`** — the dashboard's exact, hive-scoped manual hold + label; deliberately avoids the substring `hold` so it reads distinctly in + the UI, but is enforced identically by the same hold predicate. +- **`hive/`** — provenance/migration marker only; it is *not* a hold + label except as a temporary failed-migration fallback. +- **token tier** — the per-agent GitHub App installation token scope + (`advisor`, `newcomer`, `contributor`, `trusted`) matched to the agent's + policy mode; advisory tiers get read-only tokens that cannot even create + issues, and only trusted tiers get contents/PR write. See + [security-model.md § Layer + 5](/docs/hive/security-model#layer-5--github-blast-radius-controls). +- **GitHub App install scope** — the set of repositories you installed the + Forge App on; a hive can never act outside it, GitHub-side. +- **repo allowlist** — the hive-side configured repo list + (`project.repos`), enforced twice: hard, at the network policy proxy, and + again as a prompt-level `AUTHORIZED REPOS` constraint in every kick. +- **prow, tide, `/lgtm`, `/approve`** — the Kubernetes/Prow CI bot suite some + repos run alongside Hive. `tide` merges PRs that collect `lgtm`+`approved` + labels from human reviewers; it cannot gate a PR the Forge App opened, + because the App can't review its own work, so Hive's self-merge sweep + merges the App's own green PRs directly, bypassing tide. Human-queued + auto-merge (`governor.labels.automerge`, default `lgtm`) is a distinct, + human-decision-gated path. See [operator-reference.md § App self-merge + sweep](https://github.com/hivecommons/hive/blob/v5/src/docs/operator-reference.md#app-self-merge-sweep-auto_merge). +- **DCO sign-off** — the Developer Certificate of Origin trailer + (`Signed-off-by:`) every commit must carry, added by `git commit -s`; agent + policies require it, and pairs with a repo-side DCO check. See + [CONTRIBUTING.md § DCO sign-off](https://github.com/hivecommons/hive/blob/v5/CONTRIBUTING.md#dco-sign-off). +- **ClankeR, contributor trust tier (`newcomer`, `contributor`, `trusted`, + `merger`, `advisor`)** — a *different* trust model from reporter trust: it + governs what a community member's own ClankeR relay may claim from + `/contribute` (starting rate-limited, auto-promoted after 5 completed + tasks, then operator-granted upward). It never changes what a hive's own + agents may do to GitHub issues/PRs filed by anyone. See + [contributor-trust-and-roles.md](https://github.com/hivecommons/hive/blob/v5/src/docs/contributor-trust-and-roles.md). +- **governor** — the queue-depth scheduler: each eval cycle it enumerates + actionable work, applies deterministic filters, and decides which agent to + kick. See [architecture.md § 3](/docs/hive/architecture#3-the-governor-loop--from-queue-depth-to-a-kick). +- **pack** — one of the six built-in ACMM configurations (`level-1.yaml` … + `level-6.yaml`) that pairs a curated agent roster with governor cadences and + a merge policy for that level. See [agent-configuration.md § ACMM levels: + agent rosters as packs](/docs/hive/agent-configuration#acmm-levels-agent-rosters-as-packs). +- **beads** — Hive's internal work ledger (`bd` CLI): findings, tasks, and + decisions recorded as records with status, priority, and actor, independent + of GitHub issues. See [beads-cli.md](https://github.com/hivecommons/hive/blob/v5/src/docs/beads-cli.md). +- **ioscan** — the untrusted-input scanner that redacts prompt injection, + secrets, and hidden-instruction text out of GitHub issue/PR/comment content + before it reaches an agent kick; `fail_mode` defaults to `closed` (block) + at ACMM L5–L6 and `open` (redact and continue) below that. See + [ioscan.md](https://github.com/hivecommons/hive/blob/v5/src/docs/ioscan.md). + +## Where to go next + +- [Security model](/docs/hive/security-model) — the full seven-layer control map. +- [ACMM policy matrix](/docs/hive/acmm-policy-matrix) — the authoritative per-level, + per-agent capability table. +- [Security threat model](/docs/hive/security-threat-model) — attacker-oriented view: + assets, trust boundaries, threat actors, residual risks. +- [Agent configuration § Reporter + trust](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled) — + the full config reference for the reporter-trust gate. +- [Contributor trust tiers and delegated agent + roles](https://github.com/hivecommons/hive/blob/v5/src/docs/contributor-trust-and-roles.md) — ClankeR's separate trust model. +- [Hive Labels and Control Signals](https://github.com/hivecommons/hive/blob/v5/src/docs/labels-and-control-signals.md) — every + label and non-label control, what applies it, and what clears it. diff --git a/docs/content/hive/security-model.md b/docs/content/hive/security-model.md index 0356b69..1d0b709 100644 --- a/docs/content/hive/security-model.md +++ b/docs/content/hive/security-model.md @@ -8,6 +8,11 @@ Hive's answer is architectural, not aspirational. The project's core design rule Two companion pages go deeper along their own axis: [security-threat-model.md](/docs/hive/security-threat-model) is the attacker-oriented view (assets, trust boundaries, threat actors, residual risks), and [security.md](https://github.com/hivecommons/hive/blob/v5/src/docs/security.md) documents the log-scrubbing and secret-redaction layer. This page is the operator- and evaluator-facing map of the mechanisms themselves. +New to Hive and asking "who can make Hive do what in my repo?" specifically — +level, reporter trust, and repo scope working together — start with +[Securing your hive: a first-time operator's guide](/docs/hive/securing-your-hive) +instead; it links back to the mechanism pages below for the details. + ## What hive touches A running hive holds three things you care about: @@ -62,10 +67,10 @@ The strongest isolation story is around inference-backend API keys — agents li Agents are unprivileged, separated, and policed at the network layer: - **Per-agent Unix UIDs.** Each agent runs as its own user (UIDs allocated from base 2001) via `su-exec`, with its own tmux server on a per-agent socket. One agent cannot attach to another's session or signal its processes. `su-exec` is mode `4750 root:hive-launch`, so no agent UID can re-exec it to become root. -- **Per-agent GitHub tokens, not shared.** Each agent's scoped token is delivered to a per-agent cache file that is not world-readable: the entrypoint pre-creates it owned `dev:hive-` with mode `0640`, so only that agent's group can read it, and the locally-minted token path additionally writes `0600` and `chown`s to the agent's UID (deleting the file rather than leaving it shared if the chown fails). Agents cannot read each other's GitHub tokens. +- **Per-agent GitHub tokens, not shared.** Each agent's scoped token is delivered to a per-agent cache file that is not world-readable: the entrypoint pre-creates it owned `dev:hive-` with mode `0640`, so only that agent's group can read it, and the locally-minted token path additionally writes `0600` and `chown`s to the agent's UID (deleting the file rather than leaving it shared if the chown fails). Agents cannot read each other's GitHub tokens. With [proxy-side credential injection](#proxy-side-github-credential-injection) on (opt-in: `HIVE_PROXY_INJECT_GH_AUTH=true`), that cache holds only an inert placeholder and the real token never leaves the hive process. - **Per-agent tool denylists.** Declarative per-agent tool rules in `hive.yaml` become hard CLI flags at launch (`--disallowed-tools` for Claude, `--deny-tool` for Copilot). Separately, GitHub MCP *write* tools are denied for **every** agent in **every** mode — the MCP layer never grants agents a GitHub write path regardless of autonomy level; GitHub writes are instead governed by the token tier and the policy proxy below. - **Claude Remote Control is pinned off.** Since Claude Code ~2.1.226 the Remote Control bridge (session publishing to claude.ai/code) auto-starts whenever no explicit `remoteControlAtStartup` value exists — a server-side rollout flag decides, so a routine image upgrade could expose every agent as a remote-controllable session under the shared claude.ai account. Hive writes a durable `"remoteControlAtStartup": false`, re-asserted at every claude-CLI launch: merged into the shared `/data/home/.claude/settings.json` for the plain claude backend, and into the per-agent settings seed for inference-gateway agents. The merge is add-if-missing only — an operator who wants the bridge sets the key to `true` in the same file and hive never clobbers it, logging an Info receipt so an enabled bridge is always explainable — and a launch-time check warns if an agent's session file records `hasUsedRemoteControl` despite the pin. -- **A default-on GitHub policy proxy.** Every agent's `HTTPS_PROXY` points at an in-pod proxy that intercepts **GitHub API traffic** and enforces, deterministically, what the agent's current ACMM mode allows: REST method/path rules, GraphQL query-vs-mutation gating, and a **repository allowlist** — writes to any repo outside the hive's configured list are blocked with `403` (`X-Hive-Proxy-Blocked`) at the network layer, regardless of what the agent was prompted (or prompt-injected) to do. The proxy MITM-inspects only `api.github.com`; other destinations are tunneled without inspection. The same proxy attributes traffic to agents by UID (read from `/proc/net/tcp`, unforgeable) for token accounting. +- **A default-on GitHub policy proxy.** Every agent's `HTTPS_PROXY` points at an in-pod proxy that intercepts **GitHub API traffic** and enforces, deterministically, what the agent's current ACMM mode allows: REST method/path rules, GraphQL query-vs-mutation gating, and a **repository allowlist** - writes to any repo outside the hive's configured list are blocked with `403` (`X-Hive-Proxy-Blocked`) at the network layer, regardless of what the agent was prompted (or prompt-injected) to do. The proxy MITM-inspects only `api.github.com` (plus every GitHub-family host when [proxy-side credential injection](#proxy-side-github-credential-injection) is on); other destinations are tunneled without inspection. The same proxy attributes traffic to agents by UID (read from `/proc/net/tcp`, unforgeable) for token accounting. ## Layer 5 — GitHub blast-radius controls @@ -73,7 +78,8 @@ What can agents actually do to your repositories? As little as you've dialed in: - **GitHub App scoping.** The recommended auth is a GitHub App — its reach is inherently limited to the repositories you installed it on. On top of that, hive mints **per-tier installation tokens** matched to each agent's autonomy mode: advisory agents get metadata/PR read-only tokens that *cannot* create issues; mid-tier agents get issues-only tokens with no code access; only trusted tiers get contents/PR write. The shared full-installation token is stripped from every agent's environment. - **Repo allowlist, enforced twice.** The configured repo list is enforced in the policy proxy (hard, network-level) and also injected into every agent kick as an explicit `AUTHORIZED REPOS` constraint (a prompt-level reinforcement of the hard control). -- **ACMM maturity levels gate autonomy.** Six levels (L1–L6) map to per-agent policy modes enforced end-to-end by token tiers and proxy rules: advisory (observe only) → measured (file issues) → hold-gated PRs → full. At **L5**, agent policies label every PR `hold` so humans batch-review and approve; the underlying guarantee is that **merge permission simply is not granted below L6** — an L5 agent's token tier and proxy rules do not allow merging, whatever its prompt says. The system proposes; humans approve. +- **ACMM maturity levels gate autonomy.** Six levels (L1–L6) map to per-agent policy modes enforced end-to-end by token tiers and proxy rules: advisory (observe only) → measured (file issues) → hold-gated PRs → full. At **L5**, the PR-request watcher labels every agent PR with literal `hold` so humans batch-review and approve; dashboard manual holds use `hive-pause/`, and `hive/` is provenance only; the underlying guarantee is that **merge permission simply is not granted below L6** — an L5 agent's token tier and proxy rules do not allow merging, whatever its prompt says. The system proposes; humans approve. +- **Reporter trust (opt-in).** Who *asked* is a control too. With `project.issue_filter.reporter_trust.enabled: true`, issues from reporters outside the trusted set (by default GitHub owners, members and collaborators, plus an explicit login list) are not worked until a maintainer adds a triage label, and a PR whose rationale traces to such an issue is held for a human at **every** level, L6 included — so autonomy never extends to merging a stranger's request unattended. Maintainer-filed issues flow as before. Off by default; see [agent-configuration.md](/docs/hive/agent-configuration#reporter-trust-who-filed-it-not-only-what-it-is-labelled) ([#9665](https://github.com/hivecommons/hive/issues/9665)). - **DCO sign-off.** Agent policies require DCO-signed commits (`git commit -s`); pair this with a DCO check on your repos to make it a hard gate. - **Proposed gate-integrity invariants.** The [gate-integrity design](https://github.com/hivecommons/hive/blob/v5/src/docs/design/gate-integrity-invariants.md) proposes that the GitHub App write gate / push proxy refuse agent history rewrites on branches the lane did not create, refuse agent sign-off trailers added to other authors' commits, and treat gate manipulation as an ACMM regression that demotes the lane to hold-gated pending human review. @@ -130,7 +136,25 @@ Two gotchas: The provisioning template (`k8sManifestTemplate` in `src/pkg/hub/saas_provision.go`) is `kubectl apply`ed once, when a hive is provisioned. A change to it is born onto every spoke created afterwards and reaches **no spoke that already exists** — those keep whatever object the template rendered on their day. The per-hive env sweep above, the NET_ADMIN sweep, and the vanity-host patch are the reconcilers that close such gaps for the objects they own; a change to any other existing object needs one too, or the PR must say that existing spokes have to be re-provisioned. -The case that made this rule: [#7457](https://github.com/hivecommons/hive/pull/7457) added `auth-url` / `auth-response-headers` to the `hive-contribute` Ingress so a signed-in visitor's identity reaches `/api/contribute/me`. The code half rolled out with the next image; the Ingress half reached only newly provisioned spokes, and `/api/contribute/me` kept answering `401` everywhere else ([#7517](https://github.com/hivecommons/hive/issues/7517)). The hub now reconciles those two annotations onto every hosted spoke's `hive-contribute` Ingress on nginx clusters (a 15-minute sweep, `contribute_ingress_reconcile.go`; a merge patch on the annotations, which rolls no pod). OpenShift-Route clusters have no nginx Ingress and are skipped. +The case that made this rule: [#7457](https://github.com/hivecommons/hive/pull/7457) added `auth-url` / `auth-response-headers` to the `hive-contribute` Ingress so a signed-in visitor's identity reaches `/api/contribute/me`. The code half rolled out with the next image; the Ingress half reached only newly provisioned spokes, and `/api/contribute/me` kept answering `401` everywhere else ([#7517](https://github.com/hivecommons/hive/issues/7517)). The hub now reconciles those two annotations onto every hosted spoke's `hive-contribute` Ingress on nginx clusters (a 15-minute sweep, `contribute_ingress_reconcile.go`; a merge patch on the annotations, which rolls no pod). The same sweep converges the `hive-terminal` Ingress (`auth-url`, `auth-signin`, `auth-response-headers`), which drifted when the hub's public host changed and left `/terminal` answering `500` on older spokes. OpenShift-Route clusters have no nginx Ingress and are skipped. + +### Proxy-side GitHub credential injection + +`HIVE_PROXY_INJECT_GH_AUTH=true` ([#1861](https://github.com/hivecommons/hive/issues/1861)) moves every agent's usable GitHub credential out of the agent's reach. The hive mints each agent's tier-scoped App token as before, but keeps it in an in-memory registry inside the hive process; the agent's readable token cache receives only the placeholder `hive-proxy-injected-`, and so do the `GH_TOKEN`/`GITHUB_TOKEN` values derived from that cache. Any real `GH_TOKEN`, `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN` or `GITHUB_ENTERPRISE_TOKEN` in the hive process's own environment is removed from every agent's tmux session, push-capable agents included, so no agent env var carries a usable GitHub token. The MITM proxy strips whatever `Authorization` an agent sends to a GitHub host and attaches the real token of the agent it identified by UID. A prompt-injected agent that prints its token leaks a string that authenticates nowhere, and a credential smuggled out of a backend CLI's `/proc//environ` is stripped at the proxy before GitHub sees it (on spokes where forced egress is enforced, so every GitHub request passes the proxy). The rewrite is limited to GitHub hosts: a request bound for Linear, which the same proxy inspects, never carries an agent's GitHub token ([#9586](https://github.com/hivecommons/hive/issues/9586)). + +Who has it on: + +- **Nobody by default. Injection is opt-in only.** It is on only on a spoke whose Deployment sets `HIVE_PROXY_INJECT_GH_AUTH=true`. Unset is off on every hive - hosted or self-hosted, GitHub App or PAT, spoke or hub - and the hub renders no value onto newly provisioned spokes. A brief default-on for hosted App spokes ([#9597](https://github.com/hivecommons/hive/pull/9597), [#9625](https://github.com/hivecommons/hive/pull/9625)) was reverted before any spoke ran it ([#9586](https://github.com/hivecommons/hive/issues/9586)). +- **Opting a spoke in:** set `HIVE_PROXY_INJECT_GH_AUTH=true` on its Deployment. Injection needs GitHub App auth (the App-minted per-agent tokens are what the proxy injects); on a PAT-only spoke there is nothing to inject. The spoke logs one line at boot saying which way it resolved and why, for example `proxy GitHub auth injection: off (opt-in: set HIVE_PROXY_INJECT_GH_AUTH=true)`, and the dashboard Security tab shows the same state (`security.credentialInjection` in `GET /api/config/governor`, and "GitHub auth proxy-injected / agent-held" in the posture strip). +- **Copilot auth exchange is passed through.** The Copilot CLI trades the user's Copilot OAuth token (`COPILOT_GITHUB_TOKEN` / the device-flow token in `/data/copilot-user-token`) for its session token at `/copilot_internal/` on the GitHub API host (`/api/v3/copilot_internal/` on GHE). An App installation token cannot perform that exchange, so under injection the proxy leaves the agent's own `Authorization` untouched on those paths - nothing stripped, nothing injected. **Residual:** the Copilot user OAuth token remains agent-readable and spendable on those endpoints; moving it server-side too is a follow-up. No hub-held App token is ever attached there. +- **Explicit opt-out:** `HIVE_PROXY_INJECT_GH_AUTH=false` behaves exactly like unset (off) but records the decision on the pod spec. + +Two settings are checked at spoke startup: + +- **`true` together with `HIVE_PROXY_ADVISORY_OK=true` is refused** (exit code 19, log line `refusing to start: contradictory GitHub credential configuration`). Advisory mode lets the proxy trust a self-asserted `Proxy-Authorization` agent name when UID identification fails. Under injection that name picks whose real token is attached, so a caller could claim a more privileged agent and receive its token. Restore forced egress, or unset `HIVE_PROXY_INJECT_GH_AUTH` (or set it to `false`). +- **An unrecognized value** such as `1`, `TRUE`, `yes` or `on` is **not** fatal, because spokes auto-deploy shortly after a merge and a refusal would crash-loop any spoke that already carries one. It keeps its old meaning (off, so the agent still holds its real token) and is reported loudly: an ERROR log line `GitHub credential configuration warning` at boot, and an entry in the dashboard Security tab's coherence warnings (`security.credentialWarnings` in `GET /api/config/governor`). Only unset, `true` and `false` are recognized. + +What this does not yet do, tracked in [#9586](https://github.com/hivecommons/hive/issues/9586): the token holder still lives in the hive process rather than a separate signing sidecar on an internal-only network, and the Copilot user OAuth token is not yet injected server-side. ### Master key rotation @@ -149,7 +173,7 @@ The entrypoint installs an iptables REDIRECT of all outbound `:443` through the - **Spokes require `CAP_NET_ADMIN` + usable iptables extensions to start enforcing.** If the chain cannot be created, or any non-optional rule (packet-mark exemption, HTTPS `REDIRECT`, or `OUTPUT` hook) cannot be appended, the entrypoint logs the exact iptables stderr, flushes the partial `HIVE_PROXY` chain, and refuses to start. Grant the capability with `--cap-add NET_ADMIN` (docker/podman) or `securityContext.capabilities.add: ["NET_ADMIN"]` (Kubernetes), and ensure the node has the required netfilter modules. The escape hatch `HIVE_PROXY_ADVISORY_OK=true` starts the spoke with egress enforcement **advisory-only** — agents can bypass the proxy — and logs a WARN saying exactly that. Chain creation retries 5× with jittered backoff so co-scheduled spokes don't fail in lockstep. This FATAL exits with a **distinct exit code, 77** (sysexits.h `EX_NOPERM`) in exactly two cases — the bounding set lacks `CAP_NET_ADMIN`, or the node's kernel is missing a required netfilter module (`xt_mark` or `xt_REDIRECT`; a #6003 preflight probes them in a throwaway chain before building the real ruleset and names the missing module in the FATAL) — so a supervisor can key off it without parsing logs; any other cause of the same FATAL still exits `1`. `xt_owner` is optional (WARN only). For the missing-module case the fix is loading the modules on the node (e.g. a MachineConfig `/etc/modules-load.d/` drop-in), not granting the capability. See [net-admin-requirement.md](/docs/hive/net-admin-requirement#when-the-container-refuses-to-start-exit-77). - **The hive binary carries no file capabilities.** Earlier channel images shipped `/usr/local/bin/hive` with a `cap_net_admin+ep` file capability, which made the kernel refuse `execve()` with a bare `Operation not permitted` wherever the container's bounding set lacked `NET_ADMIN`. The binary now execs everywhere; the entrypoint instead raises `NET_ADMIN` as an **ambient capability** at the privilege drop, gated on the bounding set actually having it. Without the grant you get a one-line NOTICE instead of a crash. -- **Agent identity is UID-based, not self-asserted.** The proxy reads `/proc/net/tcp` to resolve the calling process's UID against `uid-map.json`, which is unforgeable — a process cannot claim another UID's socket. A `Proxy-Authorization: hive ` header sent by the caller is a fallback ONLY, and by default the proxy does not trust it: with no UID map (or no match for this connection), the caller is treated as unidentified (`ADVISORY` mode, writes blocked) rather than as whatever name the header claims. `HIVE_PROXY_ADVISORY_OK=true` also permits the header fallback, for deployments (local dev, native/systemd installs with no per-agent UID separation) that have no UID map to check against. +- **Agent identity is UID-based, not self-asserted.** The proxy reads `/proc/net/tcp` to resolve the calling process's UID against `uid-map.json`, which is unforgeable — a process cannot claim another UID's socket. A `Proxy-Authorization: hive ` header sent by the caller is a fallback ONLY, and by default the proxy does not trust it: with no UID map (or no match for this connection), the caller is treated as unidentified (`ADVISORY` mode, writes blocked) rather than as whatever name the header claims. The entrypoint persists the allocation in `/data/.hive/uid-map.json` and writes a runtime copy to `/var/run/hive/uid-map.json`; existing agents keep their UID forever, new agents append after the current maximum UID, and first boot without a persisted map adopts unique existing per-agent directory owners before assigning new IDs. `HIVE_PROXY_ADVISORY_OK=true` also permits the header fallback, for deployments (local dev, native/systemd installs with no per-agent UID separation) that have no UID map to check against. - **Rootless podman:** the image runs, but rootless cannot meaningfully grant `NET_ADMIN`, so the egress gate cannot be installed — a rootless spoke only starts with `HIVE_PROXY_ADVISORY_OK=true`, i.e. with the capability model unenforced. Treat rootless as unsupported for enforcing deployments. - **Hosted fleet:** the hub sweeps hosted spoke Deployments every 15 minutes and patches `NET_ADMIN` into the container securityContext where missing. The patch replaces the whole `securityContext` object — capabilities hand-added to a hosted spoke will be erased within 15 minutes. (On OpenShift/OVN the podspec request is necessary but not sufficient; the SCC must also allow it.) diff --git a/scripts/sync-hive-docs.main.test.ts b/scripts/sync-hive-docs.main.test.ts index 3df8390..76375ac 100644 --- a/scripts/sync-hive-docs.main.test.ts +++ b/scripts/sync-hive-docs.main.test.ts @@ -175,6 +175,10 @@ describe("sync-hive-docs main()", () => { "Edit the canonical source in the Hive repository.\n\n# README.md\n" ); + const securityGuide = readOut("securing-your-hive.md"); + expect(securityGuide).toContain("/blob/v5/src/docs/securing-your-hive.md)"); + expect(securityGuide).toContain("# securing-your-hive.md\n"); + const backup = readOut("backup-dr.md"); expect(backup).toContain("/blob/v5/src/docs/backup-restore.md)"); expect(backup).toContain("# backup-restore.md\n"); diff --git a/scripts/sync-hive-docs.test.ts b/scripts/sync-hive-docs.test.ts index 83f5514..073d586 100644 --- a/scripts/sync-hive-docs.test.ts +++ b/scripts/sync-hive-docs.test.ts @@ -21,6 +21,15 @@ describe("rewriteLinkTarget — Case 1: in-tree synced docs -> site route", () = ); }); + it.each([README, "src/docs/security-model.md"])( + "keeps the operator security guide link internal from %s", + source => { + expect(rewriteLinkTarget("securing-your-hive.md", source)).toBe( + "/docs/hive/securing-your-hive" + ); + } + ); + it("rewrites a synced ADR sibling from adr/README.md to its adr/ route", () => { expect( rewriteLinkTarget("0001-record-architecture-decisions.md", ADR_README) diff --git a/scripts/sync-hive-docs.ts b/scripts/sync-hive-docs.ts index 14be672..7c53e1e 100644 --- a/scripts/sync-hive-docs.ts +++ b/scripts/sync-hive-docs.ts @@ -32,6 +32,7 @@ const files: Array<{ source: string; target?: string }> = [ { source: "release-channels.md" }, { source: "contributor-relay.md" }, { source: "security-model.md" }, + { source: "securing-your-hive.md" }, { source: "troubleshooting.md" }, { source: "backup-restore.md", target: "backup-dr.md" }, // Third-party integration guide (hivecommons/hive#10171). diff --git a/src/__tests__/page-map-hive-arm.test.ts b/src/__tests__/page-map-hive-arm.test.ts index 19266ba..79c4814 100644 --- a/src/__tests__/page-map-hive-arm.test.ts +++ b/src/__tests__/page-map-hive-arm.test.ts @@ -51,6 +51,19 @@ describe("buildPageMap('hive') — getNavStructure switch arm", () => { expect(titles).toContain('Operations') }) + it('publishes the operator security guide in the Security section', () => { + const { pageMap, routeMap, filePaths } = buildPageMap('hive') as unknown as BuildResult + const security = pageMap.find((node) => node.name === 'Security') + expect(flatten(security?.children || [])).toContainEqual(expect.objectContaining({ + kind: 'MdxPage', + name: 'Securing your hive', + route: '/docs/hive/security/securing-your-hive', + })) + expect(routeMap['security/securing-your-hive']).toBe('securing-your-hive.md') + expect(routeMap['securing-your-hive']).toBe('securing-your-hive.md') + expect(filePaths).toContain('securing-your-hive.md') + }) + it('registers hive-specific pages such as architecture.md and governor.md', () => { const { routeMap, filePaths } = buildPageMap('hive') as unknown as BuildResult expect(filePaths).toContain('architecture.md') diff --git a/src/app/docs/page-map.ts b/src/app/docs/page-map.ts index 721797b..e590d0f 100644 --- a/src/app/docs/page-map.ts +++ b/src/app/docs/page-map.ts @@ -138,6 +138,7 @@ const NAV_STRUCTURE_HIVE: Array<{ title: string; items: NavItem[] }> = [ { title: 'Security', items: [ + { 'Securing your hive': 'securing-your-hive.md' }, { 'Security model': 'security-model.md' }, { 'Security threat model': 'security-threat-model.md' }, { 'ACMM policy matrix': 'acmm-policy-matrix.md' },