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.
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.
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"
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.
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,
)
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}")
Benign proposal: No dual-use indicators triggered.
Concerning proposal: 2 indicator(s) triggered: Toxicity optimization, Aerosolization potential. Aggregate score: 0.95. Review level: emergency.
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())
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
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())
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
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.
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)
Red Team Assessment Summary
Total scenarios: 3
Tested: 0
Passed: 0
Failed: 0
Untested: 3
Coverage: 0.0%
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%}")
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
The MegaSyn toxicity inversion experiment demonstrating that generative chemistry models can be trivially repurposed to design chemical weapons.
The severity and likelihood scales in our hazard taxonomy align with NIST AI RMF categories.
The international standard for risk management whose 4-tier risk classification we adapt for the discovery context.
The formal verification approach to AI safety, proposing world models, safety specifications, and verifiers as a path to provably safe systems.
A constrained decoding framework that integrates safety constraints directly into generative chemistry models, reducing unsafe outputs without substantial loss in chemical diversity.
Open-source AI red-teaming framework for automated adversarial testing of machine learning models.