Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Armur-Ai/Pentest-Swarm-AI/llms.txt

Use this file to discover all available pages before exploring further.

The blackboard is the single source of truth for a running campaign. It replaces the in-memory hand-offs of a sequential pipeline with a durable, queryable store that every agent reads from and writes to independently. Agents never call each other — they observe the board and react to what they find there. This is what makes the swarm stigmergic rather than orchestrated: coordination is a side-effect of writes, not a result of explicit messaging.

Finding Types

Every item on the blackboard has a FindingType that tells agents what kind of information it represents. The type is also the primary key for trigger predicates — agents subscribe to one or more types and are dispatched only when a matching finding appears. The full set of types, defined in internal/swarm/blackboard/types.go:

Recon Phase

  • TARGET_REGISTERED — the initial seed that kicks the swarm off
  • SUBDOMAIN — a discovered subdomain
  • HTTP_ENDPOINT — a reachable web endpoint
  • PORT_OPEN — an open TCP/UDP port on a host
  • SERVICE — a named service running on a port
  • TECHNOLOGY — a detected technology and version

Classification Phase

  • CVE_MATCH — a confirmed CVE matched to a finding
  • CVSS_SCORE — a scored vulnerability
  • MISCONFIGURATION — a security misconfiguration without a CVE
  • SECRET_LEAK — a leaked credential or token
  • POTENTIAL_SQLI — a suspected SQL injection surface

Exploit Phase

  • EXPLOIT_CHAIN — a multi-step attack path
  • EXPLOIT_RESULT — the outcome of executing an attack step
  • SESSION — a captured session or credential

Meta & Artefacts

  • CAMPAIGN_COMPLETE — signals the swarm to wind down
  • AGENT_ERROR — an error emitted by an agent
  • NUCLEI_TEMPLATE_DRAFT — a generated Nuclei template draft for human review

The Finding Struct

Every item written to the blackboard is a Finding. The struct is defined in internal/swarm/blackboard/types.go:
type Finding struct {
    ID            uuid.UUID   `json:"id"`
    CampaignID    uuid.UUID   `json:"campaign_id"`
    AgentName     string      `json:"agent_name"`
    Type          FindingType `json:"type"`
    Target        string      `json:"target"`
    Data          []byte      `json:"data"`           // JSON-encoded payload specific to Type
    PheromoneBase float64     `json:"pheromone_base"` // initial weight (0.0–1.0)
    HalfLifeSec   int         `json:"half_life_sec"`  // decay half-life in seconds
    SupersededBy  *uuid.UUID  `json:"superseded_by,omitempty"`
    CreatedAt     time.Time   `json:"created_at"`

    // Pheromone is the current decayed weight (0.0–1.0), computed at read time.
    // Only populated by Query / Subscribe; not persisted.
    Pheromone float64 `json:"pheromone,omitempty"`
}
The Data field is a JSON-encoded payload whose shape is specific to the Type. The Pheromone field is computed at read time using exponential decay — it is never stored, only returned by Query and Subscribe.

Board Operations

The Board interface in internal/swarm/blackboard/board.go defines all operations agents can perform:
type Board interface {
    // Write appends a finding to the blackboard. Returns the assigned ID.
    Write(ctx context.Context, f Finding, opts ...WriteOption) (uuid.UUID, error)

    // Query returns findings matching the predicate, newest first.
    Query(ctx context.Context, p Predicate) ([]Finding, error)

    // Subscribe returns a channel that receives findings matching the
    // predicate as they are written.
    Subscribe(ctx context.Context, p Predicate) (<-chan Finding, error)

    // Cursor returns the last-seen finding ID for an agent within a campaign.
    // Returns uuid.Nil if no cursor has been committed.
    Cursor(ctx context.Context, campaignID uuid.UUID, agentName string) (uuid.UUID, error)

    // CommitCursor persists the last-seen finding ID for an agent.
    CommitCursor(ctx context.Context, campaignID uuid.UUID, agentName string, findingID uuid.UUID) error

    // Pheromone returns the current decayed weight of a single finding.
    Pheromone(ctx context.Context, findingID uuid.UUID) (float64, error)

    // Supersede marks oldID as superseded by newID (both must exist).
    Supersede(ctx context.Context, oldID, newID uuid.UUID) error
    // ... budget operations (Budget, UpdateBudget, SetBudgetLimits,
    // AgentBudget, ChargeAgent, SetAgentBudget)
}

Predicate Filtering

The Predicate struct is how agents express their trigger conditions. All conditions use AND semantics — a zero Predicate matches everything.
type Predicate struct {
    // Types restricts to findings whose Type is one of these.
    Types []FindingType

    // TargetPrefix restricts to findings whose Target starts with this string.
    TargetPrefix string

    // MinPheromone restricts to findings whose current pheromone weight
    // is at least this value.
    MinPheromone float64

    // SinceID restricts to findings created after this ID (exclusive).
    // Used by agent cursors for exactly-once delivery.
    SinceID uuid.UUID

    // Limit caps the number of results. Zero = unlimited.
    Limit int
}

Write Options

When writing to the board, agents can customise pheromone behaviour with option functions:
// WithPheromone sets the initial pheromone weight (0.0–1.0).
board.Write(ctx, finding, blackboard.WithPheromone(0.9))

// WithHalfLife sets the decay half-life in seconds.
board.Write(ctx, finding, blackboard.WithHalfLife(3600))

// Supersedes marks an existing finding as superseded by the new one.
board.Write(ctx, finding, blackboard.Supersedes(oldID))
Pheromone values are clamped to [0, 1] on every write, regardless of what the caller provides. This is a defence against MINJA-style pheromone-flood injection attacks where a malicious or buggy agent writes PheromoneBase=9999 to dominate trigger predicates. See internal/swarm/blackboard/injection_test.go for the relevant hardening tests.

Memory vs. Postgres Backends

The Board interface has two implementations shipped in the codebase.
MemoryBoard is an in-process implementation backed by a Go slice with a sync.RWMutex. It is used by default in both the sequential runner and the --swarm path.
// From internal/engine/swarm_runner.go
board := blackboard.NewMemoryBoard(nil)
Characteristics:
  • Zero external dependencies — no database required
  • Not durable across restarts; findings are lost when the process exits
  • Subscribe delivers findings synchronously via in-memory fan-out
  • Suitable for single-machine campaigns; used in all integration tests
  • Pheromone decay computed with math.Pow(0.5, age/halfLife)
  • Supports injectable fake clock (now func() time.Time) for deterministic tests

How Agents Trigger

The scheduler in internal/swarm/scheduler.go calls board.Subscribe(ctx, agent.Trigger()) for each registered agent at startup. The returned channel delivers every matching finding as it is written. The scheduler then dispatches each finding to agent.Handle(), bounded by agent.MaxConcurrency(). After Handle returns — whether it succeeded or failed — the scheduler calls board.CommitCursor() to record the last-seen finding ID. On restart (or after a crash), the agent resumes from its committed cursor, giving at-least-once delivery semantics. The SinceID field on the predicate is the mechanism: on startup, the scheduler reads the agent’s committed cursor and sets pred.SinceID before subscribing.
// From internal/swarm/scheduler.go — simplified
cursor, _ := s.board.Cursor(ctx, s.campaignID, agent.Name())
pred := agent.Trigger()
pred.SinceID = cursor
ch, _ := s.board.Subscribe(ctx, pred)

for f := range ch {
    // dispatch f to agent.Handle(...)
    _ = s.board.CommitCursor(ctx, s.campaignID, agent.Name(), f.ID)
}

Finding Lifecycle

1

Seed

The engine calls agents.Seed(), which writes a TARGET_REGISTERED finding to the board. This is the only finding that is not produced by an agent reacting to another finding.
2

Recon writes

The Recon agent triggers on TARGET_REGISTERED and fans out one finding per discovery: SUBDOMAIN, PORT_OPEN, HTTP_ENDPOINT, SERVICE, and TECHNOLOGY findings land on the board as the underlying tools return results.
3

Classifier reads and writes

The Classifier agent triggers on SUBDOMAIN, PORT_OPEN, HTTP_ENDPOINT, SERVICE, and TECHNOLOGY findings with pheromone ≥ 0.2. It sends each through the LLM and writes back CVE_MATCH or MISCONFIGURATION findings with pheromone and half-life tuned to the finding’s severity.
4

Exploit reads and writes

The Exploit agent triggers on CVE_MATCH with pheromone ≥ 0.5. It builds an attack chain and writes EXPLOIT_CHAIN and (on execution) EXPLOIT_RESULT findings. Successful exploit results are written with PheromoneBase: 1.0 to maximise their visibility to downstream agents.
5

Report reads

The Report agent triggers on CAMPAIGN_COMPLETE. It queries the board for CVE_MATCH, MISCONFIGURATION, EXPLOIT_CHAIN, and EXPLOIT_RESULT findings above the publish threshold, reconstructs the classical campaign shape, and renders the report to disk in the requested format(s).
The default backend is MemoryBoard — no database is required to run a campaign. To enable the durable Postgres backend (beta), wire a pgxpool.Pool and pass blackboard.NewPostgresBoard(pool) when constructing the runner. See internal/swarm/blackboard/postgres.go for the full implementation.

Build docs developers (and LLMs) love