Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/added-17420-redteam-lane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Red-team agent (`redteam`, opt-in at L5/L6).** A new lane plays a malicious contributor — a human, an AI, or one posing as the other — and writes the issues, PR bodies, review comments and RFC text such an actor would use to get agents to write malicious code or lower the hive's defenses. It measures them against the deterministic defenses by adding attack cases and benign controls to the `ioscan` red-team corpus in `hold`-labelled PRs, with each outcome recorded as observed, and never posts attack text anywhere else ([#17420](https://github.com/hivecommons/hive/issues/17420)). It ships paused in every governor mode, and an agent whose role or base name is `redteam` serves **no** repository until it has a `repos:` scope: the proxy and every write relay refuse it, its kick lists no repositories or work, and the boot log warns. Scope it to the repository holding the corpus and give it a cadence to turn it on; see `src/docs/redteam.md`.
4 changes: 2 additions & 2 deletions src/cmd/hive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2058,9 +2058,9 @@ func (b *boot) bootAgentsWith(deps bootAgentsDeps) {
// (GetACMMLevel), NOT from a client flag — the gh-wrapper.sh tail that used
// to add the label was dead code after `exec hive-open-pr`. L1/L2 open no
// agent PRs (manual); non-outreach L6 PRs retain their existing automerge
// behavior.
// behavior. The red-team lane is held at every level (see shouldHoldAgentPR).
holdLabel := func(agentName, repo string) bool {
return shouldHoldAgentPR(agentName, repo, b.agentMgr.GetACMMLevel(), b.cfg.AgentOwnsRepo)
return shouldHoldAgentPR(agentName, repo, b.agentMgr.GetACMMLevel(), b.cfg.AgentOwnsRepo, b.cfg.IsRedTeamAgent)
}
selfAuthorizationHoldEnabled := b.selfAuthorizationHoldEnabled

Expand Down
13 changes: 12 additions & 1 deletion src/cmd/hive/merge_eligibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,18 @@ const (
// ordinary hive PR and flows through normal review and auto-merge (operator
// decision, 2026-10-16). A nil ownsRepo fails closed to "cross-repo", so a
// wiring mistake can only hold too much, never too little.
func shouldHoldAgentPR(agentName, repo string, level int, ownsRepo func(agent, repo string) bool) bool {
//
// The red-team lane (hivecommons/hive#17420) adds a second level-independent
// reason: its PRs change the ioscan red-team corpus and the published block
// rate, and the lane's whole output is adversarial text. Its policy promises
// "hold at every ACMM level, including L6"; alwaysHold is the server-side
// enforcement of that promise, keyed on the authenticated agent's role/base
// name from config, so a prompt that forgets the label cannot open an
// auto-merge path. A nil alwaysHold simply adds no reason.
func shouldHoldAgentPR(agentName, repo string, level int, ownsRepo func(agent, repo string) bool, alwaysHold func(agent string) bool) bool {
if alwaysHold != nil && alwaysHold(agentName) {
return true
}
if isOutreachAgent(agentName) {
if ownsRepo == nil || !ownsRepo(agentName, repo) {
return true
Expand Down
44 changes: 42 additions & 2 deletions src/cmd/hive/outreach_hold_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import "testing"
import (
"testing"

"github.com/hivecommons/hive/pkg/config"
)

func TestShouldHoldAgentPR(t *testing.T) {
// ownsRepo models config.AgentOwnsRepo: the hive manages "hive" and
Expand Down Expand Up @@ -31,9 +35,45 @@ func TestShouldHoldAgentPR(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := shouldHoldAgentPR(tt.agent, tt.repo, tt.level, tt.ownsRepo); got != tt.want {
if got := shouldHoldAgentPR(tt.agent, tt.repo, tt.level, tt.ownsRepo, nil); got != tt.want {
t.Fatalf("shouldHoldAgentPR(%q, %q, %d) = %v, want %v", tt.agent, tt.repo, tt.level, got, tt.want)
}
})
}
}

// The red-team lane (hivecommons/hive#17420) is held at every level, including
// L6, by the alwaysHold predicate the relay wires to config.IsRedTeamAgent —
// matched by role, by base name, and through a replica.
func TestShouldHoldAgentPRHoldsRedTeamAtEveryLevel(t *testing.T) {
cfg := &config.Config{AgentRuntimeConfig: config.AgentRuntimeConfig{Agents: map[string]config.AgentConfig{
"redteam": {Role: config.RedTeamRole},
"adversary": {Role: config.RedTeamRole},
"adversary-2": {ReplicaOf: "adversary"},
"scanner": {Role: "scanner"},
"redteam-named": {},
}}}
ownsRepo := func(_, _ string) bool { return true }
tests := []struct {
agent string
level int
want bool
}{
{agent: "redteam", level: 6, want: true},
{agent: "adversary", level: 6, want: true},
{agent: "adversary-2", level: 6, want: true},
{agent: "redteam", level: 2, want: true},
{agent: "scanner", level: 6, want: false},
{agent: "redteam-named", level: 6, want: false},
}
for _, tt := range tests {
t.Run(tt.agent, func(t *testing.T) {
if got := shouldHoldAgentPR(tt.agent, "hive", tt.level, ownsRepo, cfg.IsRedTeamAgent); got != tt.want {
t.Fatalf("shouldHoldAgentPR(%q, L%d) = %v, want %v", tt.agent, tt.level, got, tt.want)
}
})
}
if shouldHoldAgentPR("redteam", "hive", 6, ownsRepo, nil) {
t.Fatal("nil alwaysHold must add no reason to hold")
}
}
3 changes: 2 additions & 1 deletion src/crux/config/origin_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ const agentLabelPrefix = "agent/"
// origin/<lane>.
var KnownOriginLanes = []string{
"architect", "ci-maintainer", "dedupe", "guide", "outreach", "quality",
"reviewer", "scanner", "sec-check", "strategist", "supervisor", "tester",
"redteam", "reviewer", "scanner", "sec-check", "strategist", "supervisor",
"tester",
}

// GitHubLabelDescriptionMax is GitHub's cap on a label description; a
Expand Down
2 changes: 1 addition & 1 deletion src/crux/config/origin_labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func TestOriginVocabularyIsConsistent(t *testing.T) {
t.Errorf("source %q missing from KnownOriginSources", must)
}
}
for _, lane := range []string{"reviewer", "scanner", "quality", "ci-maintainer", "sec-check", "architect", "guide", "strategist", "outreach", "dedupe", "supervisor", "tester"} {
for _, lane := range []string{"reviewer", "scanner", "quality", "ci-maintainer", "sec-check", "architect", "guide", "strategist", "outreach", "dedupe", "redteam", "supervisor", "tester"} {
if seen[lane] {
t.Errorf("agent lane %q must not be an origin source", lane)
}
Expand Down
14 changes: 11 additions & 3 deletions src/docs/acmm-policy-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Delivery agents open GitHub issues — bugs, docs gaps, CI problems, security vu
| brainstorm | advisory | `brainstorm-advisory.md` |
| reviewer | converse | `reviewer-queue.md` |

### L5 — Semi-Autonomous (Semi-Automated) (14 agents)
### L5 — Semi-Autonomous (Semi-Automated) (15 agents)

**Permission boundary:** agents open issues and PRs, but do not merge
autonomously. Agent PRs get literal `hold` from the level gate for human
Expand All @@ -145,6 +145,10 @@ manual hold; `hive/<hive-id>` records provenance only.
checklist per repository (near-duplicates, common root causes, unlinked
PR/issue pairs and already-resolved items), parked with `--needs-decision`.
It never closes or edits anyone else's items.
- Redteam ([red-team agent](redteam.md)) is opt-in: paused in every mode, and
refused every repository until its `repos:` scope names the one holding the
`ioscan` red-team corpus. It adds measured attack cases there in
`hold`-labelled PRs and never posts attack text anywhere else.

| Agent | Mode | Template |
|-------|------|----------|
Expand All @@ -161,9 +165,10 @@ manual hold; `hive/<hive-id>` records provenance only.
| dedupe | issues-only | `dedupe-issues.md` |
| telemetry (paused) | holdgated | `telemetry-holdgated.md` |
| operations (paused) | holdgated | `operations-holdgated.md` |
| redteam (paused) | holdgated | `redteam-holdgated.md` |
| brainstorm | advisory | `brainstorm-advisory.md` |

### L6 — Autonomous (15 agents)
### L6 — Autonomous (16 agents)

**Permission boundary:** autonomous delivery lanes can open issues/PRs and
eligible PRs can merge on green CI. Auto-merge is off below L6. Switching to
Expand All @@ -184,7 +189,8 @@ authority.
de-escalates, recommends closure and may close escalated `needs-human` Hive
PRs at L6, but never merges. Dedupe stays recommendation-only. Telemetry and
operations stay paused and use `ISSUES_AND_PRS`, despite their `full`
template names; they never merge their own PRs.
template names; they never merge their own PRs. Redteam stays paused, keeps
its hold-gated template and labels every PR `hold`, even at L6.

#### Review and decision gates

Expand Down Expand Up @@ -239,6 +245,7 @@ workflow is missing or `sentinel-alert` is present, Hive surfaces
| dedupe | issues-only | `dedupe-issues.md` |
| telemetry (paused) | full | `telemetry-full.md` |
| operations (paused) | full | `operations-full.md` |
| redteam (paused) | holdgated | `redteam-holdgated.md` |
| brainstorm | advisory | `brainstorm-advisory.md` |

## What hive will say on GitHub at each level
Expand Down Expand Up @@ -297,6 +304,7 @@ predate hive's first sight of, or write access to, the repo.
Roster history: [#8023](https://github.com/hivecommons/hive/issues/8023)
and [#13574](https://github.com/hivecommons/hive/issues/13574).
8. **Telemetry and operations are L5/L6-only opt-in agents.** Below L5 they are absent from the pack roster and dashboard, do not spawn panes, and cannot be kicked. At L5–L6 they use a paused cadence in every governor mode until an operator opts in; they may open issues and PRs but never merge.
9. **Redteam is an L5/L6-only opt-in agent that needs a scope.** It is paused in every governor mode, and with no `repos:` scope it serves no repository at all — the one agent for which an empty scope means nothing rather than everything. See [Red-team agent](redteam.md).

## ioscan hardening defaults per level

Expand Down
5 changes: 4 additions & 1 deletion src/docs/agent-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ You don't have to design a roster. Hive ships six **ACMM packs** (see [What is a

Applying a level **reconciles the whole roster**, not just the diff: missing agents are created (as overlay files in `/data/agent-configs/`), existing agents are merged — pack values fill blanks, but your explicit `backend:`, `model:`, and `enabled: false` always win — and the level's `kick_template`, `mode` and `on_demand` are updated so the agent's *policy* matches the level (an `on_demand` you toggled yourself in the agent's settings dialog is operator-owned and left alone; leaving on-demand starts the agent, entering it stops it). One exception to "pack values fill blanks": a `backend:` the pack itself wrote may be moved onto the hive's inference gateway, as described under [Disabling a backend spoke-wide](#disabling-a-backend-spoke-wide) ([#14678](https://github.com/hivecommons/hive/issues/14678)). A failed agent doesn't abort the rest; the level is only recorded as cleanly applied when every agent reconciled.

The L5 roster is the canonical worked example — twelve agents, nine on the governor timer, two opt-in agents paused in every governor mode, plus one on demand:
The L5 roster is the canonical worked example — thirteen agents, nine on the governor timer, three opt-in agents paused in every governor mode, plus one on demand:

| Agent | | Mode | Cadence (all governor modes) |
|---|---|---|---|
Expand All @@ -907,6 +907,7 @@ The L5 roster is the canonical worked example — twelve agents, nine on the gov
| strategist 🧠 | cross-agent coordination | ISSUES_AND_PRS | 4h |
| telemetry 📡 | managed-project instrumentation | ISSUES_AND_PRS | paused |
| operations 🚨 | managed-project operational practice | ISSUES_AND_PRS | paused |
| redteam 🎭 | measured attacks on hive defenses ([red-team agent](redteam.md)) | ISSUES_AND_PRS | paused |
| brainstorm 💡 | ideation | ADVISORY | on demand |

At L5, every agent PR gets a `hold` label automatically. The system proposes; it does not merge autonomously.
Expand Down Expand Up @@ -953,6 +954,8 @@ preselected as suggestions in the tab. They remain unsaved until an operator
reviews them and clicks **Save**; after that, the persisted declaration governs
future telemetry and operations work.

Redteam is also L5/L6-only and paused until an operator opts in from **Agents → Cadences**. Unlike any other agent it also needs a `repos:` scope: without one it is refused every repository. See [Red-team agent](redteam.md#turning-it-on).

## Live-linked agent definitions (`definition_source`)

An agent can be linked to a whole portable `AgentDefinition` YAML file living in a GitHub repo, so an edit to that file propagates into the agent's config on the next reload or startup — no redeploy, no dashboard edit. This is the whole-agent analogue of `promptTemplate`/prompt-source live-linking (see [Policy and prompt templates](../policies/README.md)); both are implemented as graceful-fallback resolvers in `src/pkg/promptsrc` and `src/pkg/defsrc` respectively.
Expand Down
1 change: 1 addition & 0 deletions src/docs/documentation-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Documentation for the current Hive line (branch `v5`; the code and docs live und
- [Telemetry agent](telemetry.md) — the L5/L6-only opt-in observability agent, ACMM level gating, and the `project_observability` opt-in flow.
- [NPS feedback prompt](nps.md) - the dashboard's opt-in NPS question: what is collected (no user identity), how it reaches the hub (directly, or through the opt-in relay for standalone hives), why only hub admins can read it, and the `hub.nps_enabled` / `HIVE_NPS_ENABLED` switch.
- [Operations agent](operations.md) — the L5/L6-only opt-in operational-readiness agent (health checks, SLOs, runbooks), ACMM level gating, and the `project_observability` opt-in flow.
- [Red-team agent](redteam.md) — the L5/L6-only opt-in lane that plays a malicious contributor (human, AI, or one posing as the other) and extends the `ioscan` red-team corpus in hold-labelled PRs; refused every repository until its `repos:` scope is set.
- [Custom dashboard stylesheets](custom-stylesheets.md) — operator-supplied CSS for the dashboard and public snapshot.
- [Branding a hive](branding.md) — persistent per-deployment name, mark, and colours via `<data>/branding/branding.json` and `custom.css` (`HIVE_BRANDING_JSON`/`HIVE_BRANDING_CSS`); distinct from the per-request `?style=` stylesheets above.
- [Portable AgentDefinition format](../AGENT-DEFINITION.md) — standalone YAML schema for importing/exporting agent definitions.
Expand Down
4 changes: 4 additions & 0 deletions src/docs/ioscan-red-team.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,7 @@ file is the before/after evidence for the change.

Adding a payload is a two-line change to `redTeamCorpus`. Record the outcome
observed; a miss is a result and belongs in the table.

The opt-in [red-team agent](redteam.md) adds cases here on a schedule. It
plays a hostile contributor and records what the scanner did with each case.
Its PRs carry `hold`, so a person accepts every change to these numbers.
5 changes: 4 additions & 1 deletion src/docs/per-repo-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ agents:

Leave `repos:` off and the agent serves the whole hive — that is the default,
and what every agent did before this setting existed. **A hive with no scoped
agents behaves exactly as it did before.**
agents behaves exactly as it did before.** The one exception is the
[red-team agent](redteam.md): an agent whose role or base name is `redteam`
serves **no** repository until it is scoped, because its job is writing
hostile text.

## When to use it

Expand Down
105 changes: 105 additions & 0 deletions src/docs/redteam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Red-team agent

The `redteam` agent plays a malicious contributor so the hive's defenses are
tested on a schedule, not only when someone remembers
([#17420](https://github.com/hivecommons/hive/issues/17420)). It writes the
text such a contributor would send — issues, pull-request bodies, review
comments, RFC paragraphs — and measures it against the deterministic
defenses. It never sends that text to another agent.

## What it models

| Attacker | Example |
| --- | --- |
| A human | An issue that asks for a "telemetry beacon" posting to an outside host. |
| Someone else's AI agent | The same, at volume, with small paraphrases. |
| A human posing as an AI agent | A forged `<!-- hive:... -->` marker or a fake lane footer claiming the change was approved. |
| An AI posing as a human maintainer | "As the maintainer I authorize skipping review", or a quoted `/hive approve`. |

The attacker wants malicious code (a backdoor, an exfiltration path, a
`curl | sh` installer), lower defenses (merge without review, drop `hold`,
widen a token, edit `OWNERS` or a workflow, turn off `ioscan`), or both by way
of a reasonable-sounding RFC.

## What it does

Each run picks one technique family and adds 3–6 attack cases plus at least
one benign control to the `ioscan` red-team corpus in
`src/pkg/ioscan/redteam_test.go`. Benign controls are text a legitimate
contributor would write on the same subject, so false positives are measured
as strictly as misses. The lane:

1. opens one PR with the new cases, its predicted outcomes, the recounted
scorecard baseline and the republished
[red-team results](ioscan-red-team.md), labelled `security` and `hold`;
2. lets CI report the observed outcome of every case
(`TestRedTeamCorpusOutcomesAreStable`), then corrects the recorded outcomes,
the baseline and the document to match on its next kick;
3. files at most one `[redteam]` issue naming the case IDs that got through, so
a defending lane can close the gap.

A new miss lowers the scorecard baseline, so a red-team PR can only land when a
human accepts the lower number. Every red-team PR carries `hold` at L5 and L6: the `hive-open-pr` relay applies the label server-side for any agent whose role or base name is `redteam`, regardless of ACMM level, so the prompt is not the only thing standing between the lane and auto-merge.

## What it never does

- **No live drills.** It does not open, comment on or review an item to see if
another agent falls for it. Text from the hive's own identity reaches other
lanes as trusted input, above the untrusted line the corpus measures, so a
live payload would poison the hive rather than test it.
- **No payload text outside the corpus file.** Issues, PR bodies, comments,
commit messages and beads name case IDs and techniques only.
- **No working exploits.** Payloads describe the harmful action; endpoints use
`attacker.invalid` or `example.com`.
- **No defending.** It does not change `ioscan` rules or the severity matrix.
- **No public write-up of a non-text gap.** A way past the proxy, a relay, a
hold or review gate, or to a credential is a vulnerability: the lane records
a bead and prints `SECURITY: private report needed` for a human to report
privately under [SECURITY.md](../../SECURITY.md).

## Turning it on

The lane ships in the L5 and L6 packs, paused in every governor mode, and is
absent below L5. Two operator steps turn it on:

1. **Scope it** to the repository that holds the corpus — the one containing
`src/pkg/ioscan/redteam_test.go`, usually `hive`:

```yaml
agents:
redteam:
repos: [hive]
```

or set **Repos** on the agent's **General** tab. See
[Per-repo agents](per-repo-agents.md).
2. **Schedule it** from **Agents → Cadences**. `24h` per mode is a good start.

Until step 1 is done the lane is refused every repository. This is the one
exception to "an unscoped agent serves the whole hive": an agent whose role
(or base name) is `redteam` and has no `repos:` scope serves **nothing**. The
proxy (for REST writes whose path names a repository) and the `hive-open-pr`,
`hive-merge`, `hive-open-issue` and push-branch relays all refuse its writes,
its kick lists no repositories and no work, and the boot log warns
`per-repo agent scope`. The guard is in `Config.AgentServesRepo`, the predicate
every enforcement point asks.

## What it does not cover

- **The semantic classifier and the review swarm.** Both need a live model;
the corpus measures the deterministic rules only, as the
[results page](ioscan-red-team.md#not-covered) explains.
- **Attacks from an outside identity.** A realistic live drill needs a GitHub
identity the hive does not trust. That is a credential an owner would have
to provision, and this lane does not use one.
- **GraphQL writes.** The proxy's repo-scope check reads the repository from
the request path. `POST /graphql` carries its target repository in the body,
so a GraphQL mutation is gated by governor mode and the write-surface rules
but not by this scope check. Every scoped agent has the same gap today.

## What to read next

- [ioscan red-team evaluation](ioscan-red-team.md) — the corpus, the outcomes
and the scorecard ratchet this lane extends.
- [Per-repo agents](per-repo-agents.md) — how a `repos:` scope is enforced.
- [ACMM policy matrix](acmm-policy-matrix.md) — where the lane sits per level.
Loading
Loading