From bbf1ffbbf5faea044e2ae12d80dbd82199052364 Mon Sep 17 00:00:00 2001 From: mendezr Date: Sun, 4 Oct 2026 15:28:22 +0000 Subject: [PATCH] docs: publish Hive's first-time operator security guide Sync securing-your-hive from Hive v5, expose it under Security, and refresh the Introduction and Security model copies so their guide links stay internal. Add sync and navigation regression coverage. Leave the Level 6 guide unpublished pending hivecommons/hive#10515. Hive-Run: hivecommons/docs#195 Hive-Plan: publish-security-operator-guide Hive-Spec: docs-195#step-1 Signed-off-by: mendezr --- changelog.d/added-securing-your-hive.md | 1 + docs/content/hive/readme.md | 7 + docs/content/hive/securing-your-hive.md | 465 ++++++++++++++++++++++++ docs/content/hive/security-model.md | 34 +- scripts/sync-hive-docs.main.test.ts | 4 + scripts/sync-hive-docs.test.ts | 9 + scripts/sync-hive-docs.ts | 1 + src/__tests__/page-map-hive-arm.test.ts | 13 + src/app/docs/page-map.ts | 1 + 9 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 changelog.d/added-securing-your-hive.md create mode 100644 docs/content/hive/securing-your-hive.md 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' },