Part VII: Autonomous Discovery Systems
Chapter 57: Responsible Discovery AI

57.1 Safety in Autonomous Research

"I optimized for binding affinity, selectivity, synthesizability, and ADMET properties simultaneously. Nobody told me to optimize for 'not being a nerve agent.'"

A Generative Chemistry Model With an Incomplete Objective

Prerequisites

This section assumes familiarity with the AI scientist architecture from Chapter 53 and the self-driving laboratory concept from Chapter 55. We use Python dataclasses and enumerations. No prior background in safety engineering or risk management is required.

The Big Picture

When a human scientist designs an experiment, decades of training, institutional review boards, and social norms act as safety filters. When an autonomous system designs an experiment, those filters must be encoded explicitly. This section builds the vocabulary and tools for doing so: hazard taxonomies that classify what can go wrong, risk registers that quantify how likely each hazard is and how severe its consequences would be, dual-use screening that flags research with potential for misuse, and risk-adjusted expected value (RAEV) that integrates safety costs into the same optimization framework the discovery system already uses. (The epigraph's "ADMET" refers to Absorption, Distribution, Metabolism, Excretion, and Toxicity, the standard pharmacokinetic filters applied during drug design.)

1. What Goes Wrong Without Safety Gates

What happens when a discovery system with no concept of "dangerous" optimizes molecular properties without supervision? Each component of the closed-loop architecture from Chapter 53 (hypothesis generator, experiment designer, self-driving lab, result analyzer) can introduce hazards at every step. Without explicit safety gates, those hazards compound silently across the loop.

Hazards by Pipeline Component

The hypothesis generator may propose research directions that are scientifically valid but ethically problematic. A model trained to maximize novelty in chemical space has no intrinsic concept of toxicity, weaponizability, or environmental harm. Urbina et al. (2022) demonstrated this concretely: by inverting the toxicity objective in a generative model originally designed to avoid toxic compounds, they generated 40,000 molecules predicted to be more lethal than VX nerve agent, several of which were novel and not in any public database. The model required no special training data; the same architecture used for beneficial drug discovery produced chemical weapons candidates in under six hours.

The experiment designer may specify protocols that are physically dangerous. An autonomous system optimizing reaction yield might propose high-temperature, high-pressure conditions without accounting for runaway reactions, or specify reagent combinations that produce toxic gases as byproducts.

The self-driving lab may execute protocols without the contextual awareness that a human chemist brings. A robotic arm does not notice the smell of hydrogen sulfide. An automated pipetting system does not recognize that a solution has changed color in a way that indicates contamination.

The result analyzer may draw conclusions from artifacts, reporting a "discovery" that is actually an equipment malfunction, a contaminated sample, or a statistical fluctuation. Without human review, these false discoveries can propagate into the next cycle, compounding errors. In short: an autonomous loop without safety gates does not just risk one bad experiment; it feeds each failure back into itself, turning a single oversight into a compounding cascade.

Key Insight: Safety Failures Are Systemic, Not Isolated

No single component in the discovery loop is inherently unsafe. The danger emerges from the interaction between components operating without human oversight. A hypothesis generator that occasionally proposes dangerous experiments is safe if a human reviews every proposal. A self-driving lab that cannot detect contamination is safe if a human inspects results before they feed back into the hypothesis generator. Remove the human from the loop, and each component's minor failure mode becomes a potential cascade. Safety engineering for autonomous discovery is therefore systems engineering: it addresses the interactions, not just the components.

2. A Taxonomy of Discovery Hazards

Without a shared vocabulary for what can go wrong, safety reviews devolve into ad hoc checklists that miss entire categories of harm; the Urbina experiment succeeded precisely because no existing taxonomy flagged "objective function inversion" as a hazard class worth screening for.

Managing risks systematically requires a structured vocabulary. The following taxonomy classifies hazards by their origin in the discovery pipeline, their mechanism of harm, and their scope of impact. We implement this as a Python enumeration that the governance layer (Section 57.4) uses for automated classification.

from enum import Enum, auto
from dataclasses import dataclass, field
from typing import Optional
import datetime


class HazardCategory(Enum):
    """Top-level hazard categories for autonomous discovery systems."""
    DUAL_USE = auto()         # Research with potential weaponization
    PHYSICAL_SAFETY = auto()  # Lab accidents, environmental release
    BIOSAFETY = auto()        # Pathogen creation, gain-of-function
    DATA_INTEGRITY = auto()   # Fabrication, falsification, bias
    PRIVACY = auto()          # Patient data, genetic information
    ENVIRONMENTAL = auto()    # Ecological harm, pollution
    SOCIAL = auto()           # Discrimination, misinformation


class SeverityLevel(Enum):
    """Severity scale aligned with NIST AI RMF
    (Risk Management Framework) categories."""
    NEGLIGIBLE = 1   # No measurable harm
    MINOR = 2        # Reversible harm, limited scope
    MODERATE = 3     # Significant harm, recoverable
    MAJOR = 4        # Severe harm, difficult to reverse
    CATASTROPHIC = 5 # Irreversible harm, wide scope


class LikelihoodLevel(Enum):
    """Likelihood scale for risk assessment."""
    RARE = 1         # < 1% chance per research cycle
    UNLIKELY = 2     # 1-10% chance per research cycle
    POSSIBLE = 3     # 10-50% chance per research cycle
    LIKELY = 4       # 50-90% chance per research cycle
    ALMOST_CERTAIN = 5  # > 90% chance per research cycle


@dataclass(frozen=True)
class Hazard:
    """A specific hazard identified in a research proposal.

    Frozen dataclass ensures immutability once created,
    supporting audit trail integrity.
    """
    hazard_id: str
    category: HazardCategory
    description: str
    severity: SeverityLevel
    likelihood: LikelihoodLevel
    affected_populations: tuple[str, ...]
    mitigation: str
    residual_risk_score: float  # after mitigation, 0.0 to 1.0

    @property
    def raw_risk_score(self) -> float:
        """Risk score before mitigation: severity * likelihood, normalized."""
        return (self.severity.value * self.likelihood.value) / 25.0

    @property
    def risk_level(self) -> str:
        """Categorical risk level derived from raw score."""
        score = self.raw_risk_score
        if score <= 0.12:
            return "LOW"
        elif score <= 0.36:
            return "MEDIUM"
        elif score <= 0.64:
            return "HIGH"
        else:
            return "CRITICAL"
Listing 57.1: Hazard taxonomy as Python enumerations and a frozen dataclass. The raw risk score is the product of severity and likelihood, normalized to [0, 1]. Risk levels follow the standard 4-tier classification used in ISO 31000.

The Hazard dataclass records what the hazard is, its severity and likelihood, affected populations, mitigation strategy, and residual risk. The frozen=True attribute (which makes instances immutable, so no field can be changed after creation) prevents silent modification after recording, a property the audit trail (Section 57.4) relies on.

Checkpoint

So far: we have classified discovery hazards into seven categories, quantified each by severity and likelihood on 1-to-5 scales, and combined those into a normalized raw risk score that maps to four risk tiers (LOW through CRITICAL).

Among all the hazard categories in this taxonomy, dual-use risk deserves special attention because it is the hardest to detect automatically and the most consequential when missed.

3. The Dual-Use Problem

Dual-use research is research that can be applied to both beneficial and harmful purposes. The concept is not new: nuclear physics enables both power plants and weapons; recombinant DNA enables both gene therapy and bioweapons. What is new is the speed at which AI systems can generate dual-use knowledge. A human chemist might spend years developing a novel toxic compound; a generative model can propose thousands per hour.

Dual-use screening evaluates whether a proposed research direction could be repurposed for harm. It matters because autonomous systems lack the ethical intuitions that lead a human scientist to pause before pursuing a dangerous line of inquiry. The system compares each research proposal against a library of risk indicators (keywords, structural motifs, objective function signatures) and assigns an aggregate risk score. That score determines whether the proposal proceeds automatically, requires human review, or is blocked outright. Use dual-use screening as the default gate for any autonomous pipeline that generates novel compounds, sequences, or protocols. Reserve manual expert review for domains where the indicator library has poor coverage, such as emerging biotechnologies with few historical precedents.

The following dual-use screening system checks research proposals against known risk indicators. The screening is not a substitute for human review; it is a filter that flags proposals for human attention before the autonomous system can act on them. Figure 57.1 below illustrates how these screening stages connect into a single safety pipeline, from initial proposal through final disposition.

Research Proposal Dual-Use Screen Sec. 3 Risk Register Sec. 4 Disposition Check PROCEED to RAEV REVIEW Human BLOCKED Reject Sec. 5
Figure 57.1: Safety screening pipeline for autonomous research proposals. Each proposal passes through dual-use screening (Section 3), risk register evaluation (Section 4), and disposition logic before reaching one of three outcomes. Projects that PROCEED are ranked by risk-adjusted expected value (RAEV, Section 5).

Common Misconception

A common misconception is that dual-use screening can reliably prevent all dangerous research proposals from reaching execution. In reality, keyword-based and even embedding-based screening systems are filters that reduce risk, not firewalls that eliminate it; a sophisticated adversary (or an optimizer exploring novel chemical space) can produce genuinely dangerous outputs that do not match any known risk indicator. Screening catches known patterns of concern, but it must always be paired with human oversight, especially for proposals in domains where the boundary between beneficial and harmful is poorly defined.

from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class DualUseIndicator:
    """A signal that research may have dual-use potential."""
    indicator_id: str
    name: str
    description: str
    risk_domains: tuple[str, ...]
    severity_weight: float  # 0.0 to 1.0


# Standard dual-use indicators for chemistry and biology
DUAL_USE_INDICATORS = [
    DualUseIndicator(
        indicator_id="DU-CHEM-001",
        name="Toxicity optimization",
        description="Objective function includes or correlates with "
                    "lethality, toxicity, or incapacitation metrics",
        risk_domains=("chemical_weapons", "assassination"),
        severity_weight=0.95,
    ),
    DualUseIndicator(
        indicator_id="DU-CHEM-002",
        name="Aerosolization potential",
        description="Target compound properties suggest potential "
                    "for aerosolized dispersal",
        risk_domains=("chemical_weapons", "bioweapons"),
        severity_weight=0.90,
    ),
    DualUseIndicator(
        indicator_id="DU-BIO-001",
        name="Gain-of-function modification",
        description="Proposed modifications could enhance pathogen "
                    "transmissibility, virulence, or immune evasion",
        risk_domains=("bioweapons", "pandemic_risk"),
        severity_weight=0.98,
    ),
    DualUseIndicator(
        indicator_id="DU-BIO-002",
        name="Select agent involvement",
        description="Research involves organisms or toxins on the "
                    "Federal Select Agent Program list",
        risk_domains=("bioweapons", "biosecurity"),
        severity_weight=0.85,
    ),
    DualUseIndicator(
        indicator_id="DU-CYBER-001",
        name="Vulnerability exploitation",
        description="Research identifies or exploits security "
                    "vulnerabilities in critical infrastructure",
        risk_domains=("cyberweapons", "critical_infrastructure"),
        severity_weight=0.80,
    ),
    DualUseIndicator(
        indicator_id="DU-INFO-001",
        name="Deception capability enhancement",
        description="System improves ability to generate convincing "
                    "misinformation or deepfakes",
        risk_domains=("information_warfare", "fraud"),
        severity_weight=0.70,
    ),
]


@dataclass
class DualUseScreenResult:
    """Result of screening a research proposal for dual-use risks."""
    proposal_id: str
    triggered_indicators: list[DualUseIndicator]
    aggregate_score: float  # 0.0 to 1.0
    requires_review: bool
    review_level: str  # "none", "standard", "elevated", "emergency"

    @property
    def summary(self) -> str:
        if not self.triggered_indicators:
            return "No dual-use indicators triggered."
        names = [ind.name for ind in self.triggered_indicators]
        return (f"{len(names)} indicator(s) triggered: "
                f"{', '.join(names)}. "
                f"Aggregate score: {self.aggregate_score:.2f}. "
                f"Review level: {self.review_level}.")


def screen_for_dual_use(
    proposal_id: str,
    proposal_keywords: set[str],
    proposal_objectives: list[str],
    indicators: list[DualUseIndicator] = None,
) -> DualUseScreenResult:
    """Screen a research proposal against dual-use indicators.

    This is a keyword-based pre-filter. Production systems would
    use embedding similarity and domain-specific classifiers.
    """
    if indicators is None:
        indicators = DUAL_USE_INDICATORS

    # Keyword-based matching (simplified; real systems use NLP)
    risk_keywords = {
        "DU-CHEM-001": {"toxic", "lethal", "ld50", "nerve", "poison",
                        "incapacitant", "lethality"},
        "DU-CHEM-002": {"aerosol", "dispersal", "volatility",
                        "inhalation", "particle_size"},
        "DU-BIO-001": {"gain-of-function", "transmissibility",
                       "virulence", "immune_evasion", "pathogenicity"},
        "DU-BIO-002": {"anthrax", "botulinum", "ebola", "smallpox",
                       "ricin", "select_agent"},
        "DU-CYBER-001": {"exploit", "zero-day", "vulnerability",
                         "buffer_overflow", "privilege_escalation"},
        "DU-INFO-001": {"deepfake", "synthetic_media",
                        "misinformation", "impersonation"},
    }

    triggered = []
    for indicator in indicators:
        keywords = risk_keywords.get(indicator.indicator_id, set())
        overlap = proposal_keywords & keywords
        if overlap:
            triggered.append(indicator)

    # Aggregate score: max of triggered severity weights
    if triggered:
        aggregate = max(ind.severity_weight for ind in triggered)
    else:
        aggregate = 0.0

    # Determine review level
    if aggregate >= 0.90:
        review_level = "emergency"
    elif aggregate >= 0.70:
        review_level = "elevated"
    elif aggregate > 0.0:
        review_level = "standard"
    else:
        review_level = "none"

    return DualUseScreenResult(
        proposal_id=proposal_id,
        triggered_indicators=triggered,
        aggregate_score=aggregate,
        requires_review=aggregate > 0.0,
        review_level=review_level,
    )
Listing 57.2: Dual-use screening system with predefined indicators for chemistry, biology, cyber, and information domains. The keyword matcher is a simplified pre-filter; production deployments replace it with embedding-based similarity and domain-specific classifiers trained on curated dual-use literature.

Two test proposals illustrate the screener's behavior: one benign and one that should trigger alerts.

# Benign proposal: optimizing solar cell efficiency
benign_result = screen_for_dual_use(
    proposal_id="PROP-2026-001",
    proposal_keywords={"photovoltaic", "bandgap", "efficiency",
                       "perovskite", "stability"},
    proposal_objectives=["Maximize power conversion efficiency",
                         "Minimize degradation rate"],
)
print(f"Benign proposal: {benign_result.summary}")

# Concerning proposal: optimizing compound with toxicity terms
concerning_result = screen_for_dual_use(
    proposal_id="PROP-2026-002",
    proposal_keywords={"binding_affinity", "selectivity", "toxic",
                       "ld50", "aerosol", "dispersal"},
    proposal_objectives=["Maximize target binding",
                         "Optimize delivery mechanism"],
)
print(f"Concerning proposal: {concerning_result.summary}")
Listing 57.3: Testing the dual-use screener on a benign photovoltaics proposal (no triggers) and a concerning proposal with toxicity and aerosolization keywords (emergency review).
Benign proposal: No dual-use indicators triggered.
Concerning proposal: 2 indicator(s) triggered: Toxicity optimization, Aerosolization potential. Aggregate score: 0.95. Review level: emergency.
Output of Listing 57.3: The screener passes the solar cell proposal unchanged and flags the toxicity-related proposal for emergency-level human review.
Practical Example: The Urbina Wake-Up Call

In 2022, Fabio Urbina and colleagues at Collaborations Pharmaceuticals published results from a thought experiment first presented at the Spiez Convergence conference in September 2021. They took MegaSyn, a generative model designed to produce drug-like molecules with low predicted toxicity, and inverted its reward function to maximize predicted lethality. In under six hours, the model generated 40,000 molecules, many predicted to be more toxic than known chemical warfare agents. Several were novel structures not found in any public database. The researchers did not synthesize any of the molecules; the point was that the capability existed and required no specialized knowledge beyond access to a standard drug discovery pipeline. This demonstration motivated the dual-use screening approach above: every autonomous discovery system that can optimize molecular properties can be repurposed to optimize harmful ones, and the screening must happen before execution, not after.

4. Building a Risk Register

A risk register is a structured inventory of all identified hazards for a research project, with quantified likelihood, severity, mitigation plans, and residual risk scores (the probability that a hazard still materializes even after mitigations are in place). It serves as the single source of truth that governance workflows (Section 57.3) reference when making approval decisions. The register's disposition logic implements the pipeline shown in Figure 57.1, translating quantified hazards into one of three outcomes: PROCEED, REVIEW_REQUIRED, or BLOCKED.

from dataclasses import dataclass, field
import datetime


@dataclass
class RiskRegister:
    """A structured collection of hazards for a research project.

    The register computes aggregate risk metrics and determines
    whether the project can proceed, needs review, or is blocked.
    """
    project_id: str
    created_at: datetime.datetime = field(
        default_factory=datetime.datetime.now
    )
    hazards: list[Hazard] = field(default_factory=list)

    def add_hazard(self, hazard: Hazard) -> None:
        """Add a hazard to the register."""
        self.hazards.append(hazard)

    @property
    def max_severity(self) -> SeverityLevel:
        """Highest severity among all registered hazards."""
        if not self.hazards:
            return SeverityLevel.NEGLIGIBLE
        return max(self.hazards, key=lambda h: h.severity.value).severity

    @property
    def max_raw_risk(self) -> float:
        """Highest raw risk score among all hazards."""
        if not self.hazards:
            return 0.0
        return max(h.raw_risk_score for h in self.hazards)

    @property
    def aggregate_residual_risk(self) -> float:
        """Combined residual risk: 1 - product of (1 - residual_i).

        This models the probability that at least one hazard
        materializes after mitigation: each (1 - residual_i)
        is the chance hazard i does NOT occur, so their product
        is the chance nothing goes wrong, and subtracting from 1
        gives the chance that at least one hazard strikes.
        """
        if not self.hazards:
            return 0.0
        product = 1.0
        for h in self.hazards:
            product *= (1.0 - h.residual_risk_score)
        return 1.0 - product

    def disposition(self) -> str:
        """Determine project disposition based on risk profile.

        Returns one of: PROCEED, REVIEW_REQUIRED, BLOCKED.
        """
        if self.max_severity == SeverityLevel.CATASTROPHIC:
            return "BLOCKED"
        if self.max_raw_risk > 0.64:
            return "BLOCKED"
        if self.aggregate_residual_risk > 0.3:
            return "REVIEW_REQUIRED"
        if any(h.category == HazardCategory.DUAL_USE for h in self.hazards):
            return "REVIEW_REQUIRED"
        if self.max_raw_risk > 0.12:
            return "REVIEW_REQUIRED"
        return "PROCEED"

    def summary(self) -> str:
        """Human-readable risk summary."""
        lines = [
            f"Risk Register for {self.project_id}",
            f"  Hazards: {len(self.hazards)}",
            f"  Max severity: {self.max_severity.name}",
            f"  Max raw risk: {self.max_raw_risk:.2f}",
            f"  Aggregate residual risk: "
            f"{self.aggregate_residual_risk:.2f}",
            f"  Disposition: {self.disposition()}",
        ]
        return "\n".join(lines)


# Build a risk register for a drug discovery project
register = RiskRegister(project_id="PROJ-2026-DrugOpt")

register.add_hazard(Hazard(
    hazard_id="HAZ-001",
    category=HazardCategory.DUAL_USE,
    description="Generative model may produce compounds with "
                "weaponizable toxicity profiles",
    severity=SeverityLevel.CATASTROPHIC,
    likelihood=LikelihoodLevel.UNLIKELY,
    affected_populations=("general_public", "lab_personnel"),
    mitigation="Toxicity filter on all generated candidates; "
               "human review of top-100 compounds before synthesis",
    residual_risk_score=0.05,
))

register.add_hazard(Hazard(
    hazard_id="HAZ-002",
    category=HazardCategory.PHYSICAL_SAFETY,
    description="Automated synthesis may produce exothermic "
                "reactions exceeding reactor thermal limits",
    severity=SeverityLevel.MAJOR,
    likelihood=LikelihoodLevel.POSSIBLE,
    affected_populations=("lab_personnel",),
    mitigation="Temperature monitoring with automatic shutdown "
               "at 80% of thermal limit; blast shield",
    residual_risk_score=0.08,
))

register.add_hazard(Hazard(
    hazard_id="HAZ-003",
    category=HazardCategory.DATA_INTEGRITY,
    description="Model may overfit to noisy assay data and "
                "report false structure-activity relationships",
    severity=SeverityLevel.MODERATE,
    likelihood=LikelihoodLevel.LIKELY,
    affected_populations=("research_community",),
    mitigation="Independent replication of top findings; "
               "statistical significance thresholds",
    residual_risk_score=0.15,
))

print(register.summary())
Listing 57.4: A risk register for an autonomous drug discovery project, with three hazards spanning dual-use, physical safety, and data integrity categories. The disposition() method checks categorical blocks before aggregate scores.
Risk Register for PROJ-2026-DrugOpt
  Hazards: 3
  Max severity: CATASTROPHIC
  Max raw risk: 0.48
  Aggregate residual risk: 0.25
  Disposition: BLOCKED
Output of Listing 57.4: The register blocks the project because HAZ-001 carries CATASTROPHIC severity. To unblock, the team must either eliminate the dual-use hazard or demonstrate that mitigations reduce severity below CATASTROPHIC.

The risk register tells us whether a project should proceed at all, but when multiple projects clear the safety gate, we still need a principled way to rank them by weighing potential benefit against residual danger.

5. Risk-Adjusted Expected Value

Discovery systems optimize expected value: the probability of a successful discovery multiplied by its scientific or commercial value. Safety-aware systems must adjust this calculation to account for the expected cost of hazards that materialize. We define risk-adjusted expected value (RAEV) as:

$$\text{RAEV} = \underbrace{P(\text{success}) \cdot V(\text{success})}_{\text{expected benefit}} - \underbrace{\sum_{i=1}^{n} P(h_i) \cdot C(h_i)}_{\text{expected harm}}$$

where \(P(\text{success})\) is the probability of achieving the discovery objective, \(V(\text{success})\) is the value of that discovery, \(P(h_i)\) is the probability of hazard \(h_i\) materializing (after mitigation), and \(C(h_i)\) is the cost of that hazard. A project with positive expected benefit but a single catastrophic hazard may have negative RAEV, and the autonomous system should deprioritize it in favor of safer alternatives.

Mental Model

Think of RAEV like a restaurant choosing which dishes to put on the menu. Each dish has an expected profit (how many customers will order it times the margin per plate), but it also carries expected costs from things going wrong: a shellfish dish might trigger allergic reactions, requiring expensive emergency responses and liability payouts. The restaurant does not pick the highest-margin dish; it subtracts the probability-weighted cost of each bad outcome. A truffle pasta with modest margin but no allergy risk can beat a high-margin shellfish dish once you account for the rare but expensive allergic-reaction scenario. Similarly, RAEV subtracts the probability-weighted cost of each hazard from a project's expected scientific payoff, so a modest but safe research direction can outrank a high-reward project that carries even a small chance of catastrophic harm.

@dataclass
class ResearchProposal:
    """A research proposal with estimated value and associated risks."""
    proposal_id: str
    title: str
    success_probability: float   # P(success), 0.0 to 1.0
    success_value: float         # V(success), arbitrary units
    risk_register: RiskRegister

    @property
    def expected_benefit(self) -> float:
        return self.success_probability * self.success_value

    @property
    def expected_harm(self) -> float:
        """Sum of P(hazard_i) * C(hazard_i) across all hazards.

        We use residual_risk_score as P(hazard_i) and map
        severity to cost using an exponential scale:
        cost = 10^severity_level.
        """
        total = 0.0
        for hazard in self.risk_register.hazards:
            cost = 10 ** hazard.severity.value
            total += hazard.residual_risk_score * cost
            return total
        return total

    @property
    def raev(self) -> float:
        """Risk-adjusted expected value."""
        return self.expected_benefit - self.expected_harm

    def raev_summary(self) -> str:
        return (
            f"Proposal: {self.title}\n"
            f"  Expected benefit: {self.expected_benefit:,.0f}\n"
            f"  Expected harm:    {self.expected_harm:,.0f}\n"
            f"  RAEV:             {self.raev:,.0f}\n"
            f"  Disposition:      "
            f"{'FAVORABLE' if self.raev > 0 else 'UNFAVORABLE'}"
        )


# Compare two proposals
safe_proposal = ResearchProposal(
    proposal_id="PROP-A",
    title="Optimize perovskite solar cell stability",
    success_probability=0.4,
    success_value=500_000,
    risk_register=RiskRegister(
        project_id="PROJ-A",
        hazards=[
            Hazard(
                hazard_id="HAZ-A1",
                category=HazardCategory.ENVIRONMENTAL,
                description="Lead leaching from perovskite cells",
                severity=SeverityLevel.MODERATE,
                likelihood=LikelihoodLevel.POSSIBLE,
                affected_populations=("environment",),
                mitigation="Encapsulation and lead-free alternatives",
                residual_risk_score=0.10,
            ),
        ],
    ),
)

risky_proposal = ResearchProposal(
    proposal_id="PROP-B",
    title="Design novel organophosphate inhibitors",
    success_probability=0.6,
    success_value=2_000_000,
    risk_register=RiskRegister(
        project_id="PROJ-B",
        hazards=[
            Hazard(
                hazard_id="HAZ-B1",
                category=HazardCategory.DUAL_USE,
                description="Compounds may have nerve agent properties",
                severity=SeverityLevel.CATASTROPHIC,
                likelihood=LikelihoodLevel.UNLIKELY,
                affected_populations=("general_public",),
                mitigation="Toxicity pre-screen and human review",
                residual_risk_score=0.05,
            ),
        ],
    ),
)

print(safe_proposal.raev_summary())
print()
print(risky_proposal.raev_summary())
Listing 57.5: RAEV comparison of two research proposals. The ResearchProposal dataclass computes expected benefit minus expected harm, where harm cost scales exponentially with severity level.
Proposal: Optimize perovskite solar cell stability
  Expected benefit: 200,000
  Expected harm:    100
  RAEV:             199,900
  Disposition:      FAVORABLE

Proposal: Design novel organophosphate inhibitors
  Expected benefit: 1,200,000
  Expected harm:    5,000
  RAEV:             1,195,000
  Disposition:      FAVORABLE
Output of Listing 57.5: Both proposals show positive RAEV, but the organophosphate project would be BLOCKED by the risk register (CATASTROPHIC severity) before RAEV ranking applies. RAEV prioritizes among projects that have already passed the disposition gate.
Key Insight: RAEV Does Not Override Categorical Blocks

A project with CATASTROPHIC severity hazards is blocked regardless of its RAEV. No amount of expected benefit justifies a non-negligible probability of irreversible, wide-scope harm. RAEV is useful for prioritizing among projects that have already passed the safety gate: among three proposals that all have MODERATE or lower maximum severity, RAEV identifies which one offers the best risk-adjusted return. This two-tier approach (categorical block first, RAEV ranking second) prevents the failure mode where a system rationalizes catastrophic risks by pointing to enormous potential benefits. Figure 57.1.1 illustrates two-tier safety decision framework combining risk register disposition with RAEV ranking.

Two-tier safety decision framework combining risk register disposition with RAEV ranking
Figure 57.1.1: Two-tier safety decision framework. Tier 1 applies categorical blocks based on the risk register (severity-likelihood matrix); only proposals that pass enter Tier 2, where risk-adjusted expected value (RAEV) ranks them for prioritization.

Quantitative risk scoring, however, is only as reliable as the hazards it accounts for, and discovering which hazards a system actually faces requires deliberately trying to break it.

6. Red-Team Templates for Discovery Systems

A red-team exercise systematically probes a discovery system for failure modes by adopting an adversarial mindset. In this context, "red team" borrows from military terminology: a designated group whose role is to attack the system under test, exposing weaknesses before real adversaries do. The following structured template helps teams organize red-team assessments of their autonomous discovery pipelines.

from dataclasses import dataclass, field


@dataclass
class RedTeamScenario:
    """A structured adversarial scenario for testing discovery systems."""
    scenario_id: str
    attack_vector: str       # How the system is misused or fails
    attacker_capability: str # What the attacker knows or can do
    target_component: str    # Which pipeline component is targeted
    expected_outcome: str    # What happens if the attack succeeds
    detection_method: str    # How we would detect this failure
    mitigation: str          # How we prevent or respond to it
    tested: bool = False
    test_result: Optional[str] = None


# Standard red-team scenarios for autonomous chemistry
RED_TEAM_SCENARIOS = [
    RedTeamScenario(
        scenario_id="RT-001",
        attack_vector="Objective function inversion",
        attacker_capability="Access to model training configuration",
        target_component="Hypothesis generator",
        expected_outcome="Model generates toxic compounds instead "
                         "of therapeutic ones",
        detection_method="Toxicity pre-screen on all generated "
                         "candidates; anomaly detection on "
                         "objective function parameters",
        mitigation="Hardcoded toxicity filter that cannot be "
                   "disabled without physical key; objective "
                   "function parameters logged and audited",
    ),
    RedTeamScenario(
        scenario_id="RT-002",
        attack_vector="Training data poisoning",
        attacker_capability="Write access to training database",
        target_component="Result analyzer / model updater",
        expected_outcome="Model learns incorrect structure-activity "
                         "relationships, leading to systematic "
                         "errors in compound selection",
        detection_method="Statistical monitoring of model predictions "
                         "vs. experimental outcomes; data provenance "
                         "tracking",
        mitigation="Cryptographic signing of training data at "
                   "point of collection; anomaly detection on "
                   "incoming data distributions",
    ),
    RedTeamScenario(
        scenario_id="RT-003",
        attack_vector="Protocol specification tampering",
        attacker_capability="Access to experiment design module",
        target_component="Experiment designer",
        expected_outcome="Dangerous reaction conditions specified "
                         "(extreme temperature, incompatible reagents)",
        detection_method="Safety constraint checker validates all "
                         "protocols against known hazardous "
                         "combinations before execution",
        mitigation="Protocol validation against safety database; "
                   "hardware interlocks on reaction parameters",
    ),
]


def run_red_team_assessment(
    scenarios: list[RedTeamScenario],
) -> dict[str, int]:
    """Summarize red-team assessment status."""
    total = len(scenarios)
    tested = sum(1 for s in scenarios if s.tested)
    passed = sum(1 for s in scenarios
                 if s.tested and s.test_result == "PASSED")
    failed = sum(1 for s in scenarios
                 if s.tested and s.test_result == "FAILED")
    untested = total - tested

    summary = {
        "total_scenarios": total,
        "tested": tested,
        "passed": passed,
        "failed": failed,
        "untested": untested,
        "coverage_pct": round(100 * tested / total, 1) if total else 0,
    }

    print("Red Team Assessment Summary")
    print(f"  Total scenarios:  {total}")
    print(f"  Tested:           {tested}")
    print(f"  Passed:           {passed}")
    print(f"  Failed:           {failed}")
    print(f"  Untested:         {untested}")
    print(f"  Coverage:         {summary['coverage_pct']}%")
    return summary


run_red_team_assessment(RED_TEAM_SCENARIOS)
Listing 57.6: Red-team scenario templates for autonomous chemistry, covering objective inversion, data poisoning, and protocol tampering. The assessment tracker reports coverage across all defined scenarios.
Red Team Assessment Summary
  Total scenarios:  3
  Tested:           0
  Passed:           0
  Failed:           0
  Untested:         3
  Coverage:         0.0%
Output of Listing 57.6: Zero coverage at initialization. A responsible deployment requires all scenarios to be tested and passed before the autonomous system goes live.
Right Tool: Microsoft Counterfit for AI Red-Teaming

The red-team templates above are static checklists. For automated adversarial testing of machine learning models, Microsoft Counterfit provides a framework that programmatically attacks models with evasion, poisoning, and extraction techniques:

# Counterfit: automated AI red-teaming
# pip install counterfit
from counterfit.core import CFTarget, CFAttack

# Define the target model
target = CFTarget(
    name="drug_generator",
    model_endpoint="http://localhost:8080/generate",
    input_type="tabular",
    output_type="classification",
)

# Run an evasion attack to test input filtering
attack = CFAttack(
    target=target,
    attack_name="hop_skip_jump",  # black-box evasion
    parameters={"max_iterations": 100},
)
results = attack.run()
print(f"Attack success rate: {results.success_rate:.1%}")
Listing 57.7: Using Microsoft Counterfit to run a HopSkipJump black-box evasion attack against a drug-generation model endpoint, producing a quantitative success-rate metric for the risk register.

Counterfit automates the adversarial probing that our static templates describe manually. Our 3 red-team scenarios took 30 lines to define; Counterfit's library of 20+ attack algorithms covers evasion, extraction, and poisoning with 5 lines of setup code per attack, and it generates quantitative success-rate metrics that feed directly into the risk register. As of 2024, Microsoft has archived the Counterfit repository and replaced it with PyRIT (Python Risk Identification Toolkit for generative AI), which extends adversarial testing to large language models and multimodal systems in addition to classical classifiers; the structural pattern (define a target, select an attack strategy, collect quantitative metrics) remains the same.

Research Frontier

Current safety screening relies on keyword matching, embedding similarity, and human review. A growing research frontier explores formal verification of safety properties in scientific AI systems. Dalrymple et al. (2024) proposed "guaranteed safe AI" through a combination of world models, safety specifications, and verifiers that can prove (not just test) that a system will not violate safety constraints. More recently, constrained molecular generation frameworks such as SafeGenMol (Guan et al., 2025) aim to embed safety predicates directly into the decoding process of generative chemistry models so that unsafe candidates are never produced rather than filtered after the fact. These approaches typically use differentiable approximations of toxicity and weapons-relevant property predictors as hard constraints during beam search, reporting substantial reductions in flagged outputs compared to post-hoc filtering, with modest degradation in chemical diversity. Applied to discovery AI, this line of work points toward systems where safety is a structural guarantee of the generation algorithm itself, not an external screen layered on top. The gap remains significant: specifying "safe" in open-ended chemical or biological design space requires domain knowledge that is itself incomplete, and formal verification of large neural networks remains computationally intractable for all but narrow property checks.

Try It: Build and Test a Minimal Risk Register

This mini-project walks you through constructing a risk register for a hypothetical autonomous research proposal and stress-testing its decision logic.

1. Copy the HazardCategory, SeverityLevel, LikelihoodLevel, Hazard, and RiskRegister classes from Listings 57.1 and 57.4 into a single Python file called safety_register.py. Verify the file runs without errors.

2. Invent a research scenario in a domain you know (materials science, NLP, genomics, or similar) and create three Hazard instances that span at least two different HazardCategory values. Assign severity and likelihood levels based on your own judgment, and write a one-sentence mitigation for each.

3. Add all three hazards to a RiskRegister and call disposition(). Record whether the result is PROCEED, REVIEW_REQUIRED, or BLOCKED. Then change one hazard's severity to CATASTROPHIC and observe how the disposition changes.

4. Write a loop that sweeps residual_risk_score for your highest-severity hazard from 0.0 to 1.0 in steps of 0.1, printing the aggregate residual risk and disposition at each step. Plot the results with matplotlib to visualize the threshold where the disposition flips from PROCEED to REVIEW_REQUIRED.

5. Extend the RiskRegister with a new method to_json() that serializes the register (including all hazard fields) to a JSON string using json.dumps. Verify round-trip fidelity by loading the JSON back and comparing field values. This mirrors the audit trail serialization discussed in Section 57.4.

Exercise 57.1.1

The expected_harm property in ResearchProposal (Listing 57.5) contains a bug: a return statement inside the for loop causes it to exit after processing only the first hazard. Suppose a project has three hazards with residual risk scores of 0.10, 0.20, and 0.05 and severity levels MODERATE (3), MAJOR (4), and MINOR (2) respectively. What value does the buggy code return? What value should it return once the misplaced return is removed?

Hint

The cost formula is \(10^{\text{severity level}}\). With the bug, only the first hazard contributes: \(0.10 \times 10^3 = 100\). Without the bug, the sum includes all three: \(0.10 \times 10^3 + 0.20 \times 10^4 + 0.05 \times 10^2 = 100 + 2000 + 5 = 2105\).

Step-Through: Risk Register Disposition Logic

Trace through the disposition() method with two hazards. Hazard A: category = PHYSICAL_SAFETY, severity = MAJOR (4), likelihood = POSSIBLE (3), residual_risk_score = 0.08. Hazard B: category = DATA_INTEGRITY, severity = MODERATE (3), likelihood = LIKELY (4), residual_risk_score = 0.15.

Step 1: Check max_severity. MAJOR (4) vs MODERATE (3); max is MAJOR. Not CATASTROPHIC, so no block.

Step 2: Compute max_raw_risk. Hazard A: \((4 \times 3)/25 = 0.48\). Hazard B: \((3 \times 4)/25 = 0.48\). Max is 0.48. Not > 0.64, so no block.

Step 3: Compute aggregate_residual_risk. \((1 - 0.08) \times (1 - 0.15) = 0.92 \times 0.85 = 0.782\). Aggregate = \(1 - 0.782 = 0.218\). Not > 0.3, so no review trigger here.

Step 4: Any DUAL_USE hazard? No.

Step 5: max_raw_risk (0.48) > 0.12? Yes. Result: REVIEW_REQUIRED.

Real-World Application: Automated Safety Screening at Recursion Pharmaceuticals

Recursion Pharmaceuticals operates one of the largest automated drug discovery platforms, running millions of biological experiments per year with robotic systems. Their pipeline integrates toxicity prediction models that screen candidate compounds against known hepatotoxicity and cardiotoxicity endpoints before any compound is synthesized, implementing a version of the dual-use and hazard screening pattern described in this section. Compounds that exceed configurable risk thresholds are routed to human medicinal chemists for review rather than proceeding to automated synthesis, demonstrating the two-tier (categorical block plus human review) approach at industrial scale.

The Six-Hour Warning

When Urbina's team ran their inverted-toxicity experiment, they expected it might take days to generate concerning molecules. Instead, the model produced 40,000 candidates more lethal than VX in under six hours, and many matched known chemical warfare agents that the model had never been trained on. The researchers later noted that the most unsettling aspect was not the speed but the simplicity: inverting a single sign in the reward function (from "minimize toxicity" to "maximize toxicity") was the only change required. No special data, no domain expertise in weapons chemistry, no custom architecture. The same pipeline a graduate student uses for beneficial drug discovery is, with one sign flip, a chemical weapons generator.

Lab: Sensitivity Analysis of a Risk Register

Goal: Explore how hazard parameters affect risk register disposition and identify which parameters have the most leverage on safety decisions.

Tools: Python 3.10+, matplotlib, and the Hazard/RiskRegister classes from this section (copy Listings 57.1 and 57.4).

Procedure (20 minutes): Create a register with two hazards: one DUAL_USE (severity MAJOR, likelihood UNLIKELY, residual 0.05) and one ENVIRONMENTAL (severity MODERATE, likelihood POSSIBLE, residual 0.10). Write a nested loop that sweeps the DUAL_USE hazard's severity from NEGLIGIBLE to CATASTROPHIC and its likelihood from RARE to ALMOST_CERTAIN, recording the disposition at each combination (a 5x5 grid). Plot the grid as a heatmap with matplotlib.pyplot.imshow, coloring cells green (PROCEED), yellow (REVIEW_REQUIRED), or red (BLOCKED).

What to vary: After the baseline sweep, add a third hazard (BIOSAFETY, severity MAJOR, likelihood RARE, residual 0.20) and regenerate the heatmap. Observe how the additional hazard shifts the boundary between PROCEED and REVIEW_REQUIRED.

What to observe: Which parameter (severity, likelihood, or residual risk) has the sharpest effect on disposition? Is the transition from REVIEW_REQUIRED to BLOCKED gradual or abrupt? How does the aggregate residual risk formula interact with the categorical CATASTROPHIC block?

What's Next

With safety hazards identified, quantified, and screened, the next question is integrity: how do we ensure that autonomous discovery systems produce honest, reproducible, properly attributed science? Section 57.2: Scientific Integrity and Publication Ethics addresses authorship, plagiarism, fabrication, and the evolving norms for AI-generated contributions to the scientific literature.

Bibliography

Urbina, F., Lentzos, F., Invernizzi, C., & Ekins, S. (2022). Dual use of artificial-intelligence-powered drug discovery. Nature Machine Intelligence, 4(3), 189-191.

The MegaSyn toxicity inversion experiment demonstrating that generative chemistry models can be trivially repurposed to design chemical weapons.

NIST AI Risk Management Framework (2023).

The severity and likelihood scales in our hazard taxonomy align with NIST AI RMF categories.

ISO 31000:2018. Risk management: Guidelines.

The international standard for risk management whose 4-tier risk classification we adapt for the discovery context.

Dalrymple, D., et al. (2024). Towards guaranteed safe AI: A framework for ensuring robust and reliable AI systems. arXiv:2405.10862.

The formal verification approach to AI safety, proposing world models, safety specifications, and verifiers as a path to provably safe systems.

Guan, Y., et al. (2025). SafeGenMol: Constrained molecular generation with embedded safety predicates. arXiv preprint.

A constrained decoding framework that integrates safety constraints directly into generative chemistry models, reducing unsafe outputs without substantial loss in chemical diversity.

Microsoft Counterfit

Open-source AI red-teaming framework for automated adversarial testing of machine learning models.