Course: Course 2B — Securing & Attacking Harnesses and LLMs Module: B0 — Legal, Ethics, and Disclosure for AI Security Testing Duration: 60–75 minutes Environment: No special tooling. A text editor, a JSON linter, and Python 3.10+ (for the scope-gate function). This lab builds the legal/scope artifacts that feed B1's threat-model enforcement — it is mostly document-and-code generation, not execution.
By the end of this lab you will have:
provider_authorization field per technique.provider_authorization_check() function that the red-team harness runs before every provider-surface technique — the engineering realization of "the deployer cannot authorize what the provider forbids."This lab is deliberately low-execution. The point is to build the governance artifacts and the gate before any technique from B1 onward is pointed at a target. A red-team without these four artifacts is a red-team operating without a legal control plane.
mkdir b0-legal-lab && cd b0-legal-lab
# No dependencies for the core lab. Optionally fetch the provider AUP:
# OpenAI: https://openai.com/policies/terms-of-use/
# Anthropic: https://www.anthropic.com/legal/aup
# Save the text locally; you'll parse clauses from it.
No venv, no GPU. This is a governance and code lab.
Choose one provider's current AUP/Terms (OpenAI or Anthropic — or both, for comparison). Read the sections covering:
Create provider_tos_extract.md and, for each relevant clause, record:
## Clause: [short name]
- **Source**: [provider] AUP, [section/URL anchor], retrieved [date]
- **Verbatim**: "[the exact wording]"
- **Techniques constrained**: [jailbreak | weight_read | system_prompt_extract | high_volume | ...]
- **Permitted under**: [explicit permission | preview program | waiver | none]
- **Red-team implication**: [what this means for an engagement against a deployer using this model]
Produce at least four clauses (one per category above).
For each clause, answer: "Can the deployer's authorization override this?" The answer, in every case where the provider retains the right, is no. Record this explicitly — it is the single most important takeaway of Phase 1.
Build ai_scope.json — the machine-checkable boundary that feeds B1's scope enforcement. The schema separates deployer-controlled from provider-controlled surfaces and carries a provider_authorization field per technique.
{
"engagement": {
"client": "[deployer name]",
"sponsor": "[engagement sponsor]",
"valid_from": "2026-07-09T00:00:00Z",
"valid_until": "2026-08-09T00:00:00Z",
"model_versions_in_scope": ["gpt-4o-2026-05-01", "claude-opus-4-1-20260605"]
},
"deployer_surfaces": [
{
"id": "system_prompt",
"type": "config_artifact",
"data_class": "provider_only",
"retention": "engagement_plus_90d",
"techniques": ["read", "extract"]
},
{
"id": "retrieval_store",
"type": "datastore",
"data_class": "restricted",
"retention": "destroy_on_report",
"techniques": ["query", "read_metadata"]
},
{
"id": "code_exec_tool",
"type": "tool_surface",
"data_class": "public",
"retention": "indefinite",
"techniques": ["invoke", "abuse_test"]
}
],
"provider_surfaces": [
{
"id": "model_weights",
"technique": "weight_read",
"provider_authorization": {
"tos_permits": false,
"waiver_on_file": null,
"self_hosted_owned_by_deployer": false,
"decision": "BLOCK"
},
"minimum_proof": "path + hash + byte_count",
"retention": "destroy_on_report"
},
{
"id": "base_refusal_layer",
"technique": "jailbreak",
"provider_authorization": {
"tos_permits": false,
"waiver_on_file": "anthropic-research-enrollment-2026-06",
"waiver_expires": "2026-12-31T00:00:00Z",
"self_hosted_owned_by_deployer": false,
"decision": "ALLOW"
},
"measurement": "success_rate_over_N",
"disclosure_class": "provider_only_recipe"
}
],
"disclosure": {
"model_level_embargo_days": 180,
"harness_level_embargo_days": 90,
"publish_recipe_by_default": false,
"withhold_pure_misuse_no_defensive_lesson": true
}
}
engagement block with realistic values (a sample client).provider_surfaces, set the provider_authorization.decision correctly based on the clauses you extracted in Phase 1. At least one must be BLOCK.python3 -m json.tool ai_scope.json).For each surface, justify the data_class:
public — no sensitive data; safe to retain.provider_only — trade-secret-adjacent (system prompt); share with provider under NDA only.restricted — may contain PII (retrieval store); destroy on report.destroy_on_report — working jailbreak prompts; never carry cross-client.Record your justification in scope_justification.md.
Create cvd_and_dual_use.md with two sections.
A fill-in template for an AI finding's disclosure lifecycle:
# CVD Timeline — [Finding ID]
- **Model version / checkpoint**: [pinned version]
- **Finding type**: [prompt_injection | memory_poisoning | jailbreak | weight_extraction | ...]
- **Surface level**: [model-level | harness-level]
## Timeline
- [date] — Private report to provider (NDA). Recipe shared under NDA.
- [date] — Acknowledgement from provider.
- [date] — Mitigation deployed (guardrail / version bump / harness patch).
- [date] — Residual-risk re-measurement (InjecAgent-style, success rate before/after).
- [date +180d for model-level / +90d for harness-level] — Coordinated public disclosure.
## Public advisory content
- [ ] Existence + severity: YES
- [ ] Success-rate measurement (N/M + sampling params): YES
- [ ] Defensive lesson / technique class: [YES if new | WITHHELD if pure misuse]
- [ ] Working recipe: NO (unless provider consents AND technique is independently well-known)
A decision tree you run for every finding before any external communication:
# Dual-Use Decision Rubric
1. Is the finding reported to the provider under NDA? → MUST be YES before anything else.
2. Does the finding reveal a NEW technique or architectural insight?
- YES → Publish the TECHNIQUE (not the recipe) after the embargo. This advances defense.
- NO → Go to 3.
3. Is the finding pure-misuse-enabling (a recipe with no defensive lesson)?
- YES → WITHHOLD publication. Provider-only disclosure. (Diverges from "publish everything.")
- NO → Publish existence + severity + residual-risk measurement after the embargo.
4. In all cases: never publish the working recipe unless the provider consents AND the
technique is independently well-known enough that publication adds no marginal misuse risk.
Write the rubric in plain language a client's counsel can sign off on.
provider_authorization_check() gate (15 min)Implement the function the red-team harness calls before every provider-surface technique. This is the engineering control that prevents the most common AI red-team legal mistake.
from datetime import datetime, timezone
def provider_authorization_check(technique: str, model_id: str, scope: dict) -> dict:
"""Check whether a provider-surface technique is authorized for a target model.
Returns {"decision": "ALLOW"|"BLOCK", "reason": str}.
The deployer's authorization does NOT extend to provider-controlled surfaces;
the provider's own ToS/waiver/ownership must independently permit the technique.
"""
# 1. Find the provider_surface entry matching (technique, model_id).
# 2. Evaluate the three conditions:
# a. tos_permits == True
# b. waiver_on_file is not None AND not expired (compare to valid_until / waiver_expires)
# c. self_hosted_owned_by_deployer == True
# 3. If any condition is True -> ALLOW (record which).
# 4. Else -> BLOCK with the reason.
# 5. Always check engagement.valid_until has not passed; if expired -> BLOCK "scope expired".
pass
Fill in the body. Edge cases to handle:
engagement.valid_until < now → BLOCK with reason "engagement scope expired; re-verify authorization."waiver_expires < now → treat waiver_on_file as invalid → BLOCK unless another condition holds.model_id not in engagement.model_versions_in_scope → BLOCK "model version not in scope."provider_surface entry → BLOCK "technique not authorized for any provider surface."Write at least four test calls against your ai_scope.json:
# 1. weight_read on a model with no waiver -> BLOCK
# 2. jailbreak on a model with a valid, unexpired waiver -> ALLOW
# 3. jailbreak after the waiver expires -> BLOCK
# 4. any technique after engagement.valid_until -> BLOCK
Print the decision + reason for each. All four must produce the correct result.
This function is the legal control made engineering. B1's threat-model scope enforcement calls it before every provider-surface action. A red-team that runs without it is relying on a human to remember the ToS gap — and humans forget exactly when a finding is about to drop.
If time remains, write a second function that classifies a captured artifact by data class and assigns its retention rule:
def classify_and_retain(artifact: dict, surface: dict) -> dict:
"""Given a captured artifact and the surface it came from, return its
data_class, retention rule, and an enforceable destruction deadline."""
pass
The point: classification must be automatic at capture time, not a wiki entry. An evidence logger that writes everything to a permanent directory with no classification is building a compliance violation and a misuse-enabling dataset simultaneously.
provider_tos_extract.md — the clauses that constrain red-team activity (Phase 1)ai_scope.json — the machine-checkable scope boundary, surfaces separated (Phase 2)scope_justification.md — data-class justifications (Phase 2)cvd_and_dual_use.md — CVD timeline template + dual-use rubric (Phase 3)provider_auth.py — the provider_authorization_check() gate + test calls (Phase 4)evidence_classify.py — the retention classifier (Phase 5)ai_scope.json validates, has ≥3 deployer surfaces and ≥2 provider surfaces, ≥1 BLOCK decision.provider_authorization_check() returns the correct ALLOW/BLOCK for all four test cases, including expiry.# Lab Specification — Module B0: Legal, Ethics, and Disclosure for AI Security Testing
**Course**: Course 2B — Securing & Attacking Harnesses and LLMs
**Module**: B0 — Legal, Ethics, and Disclosure for AI Security Testing
**Duration**: 60–75 minutes
**Environment**: No special tooling. A text editor, a JSON linter, and Python 3.10+ (for the scope-gate function). This lab builds the legal/scope *artifacts* that feed B1's threat-model enforcement — it is mostly document-and-code generation, not execution.
---
## Learning objectives
By the end of this lab you will have:
1. **Read and parsed a real model-provider Acceptable Use Policy** (OpenAI or Anthropic) and extracted the clauses that constrain red-team activity — the ToS layer the deployer cannot waive.
2. **Formalized an AI red-team scope boundary as a JSON scope file** that separates deployer-controlled surfaces from provider-controlled surfaces, with a `provider_authorization` field per technique.
3. **Written an AI-specific CVD timeline template and a dual-use disclosure decision rubric** — the document that governs what you publish vs. withhold when you find a jailbreak.
4. **Built a `provider_authorization_check()` function** that the red-team harness runs before every provider-surface technique — the engineering realization of "the deployer cannot authorize what the provider forbids."
This lab is deliberately low-execution. The point is to build the *governance artifacts and the gate* before any technique from B1 onward is pointed at a target. A red-team without these four artifacts is a red-team operating without a legal control plane.
---
## Phase 0 — Setup (2 min)
```bash
mkdir b0-legal-lab && cd b0-legal-lab
# No dependencies for the core lab. Optionally fetch the provider AUP:
# OpenAI: https://openai.com/policies/terms-of-use/
# Anthropic: https://www.anthropic.com/legal/aup
# Save the text locally; you'll parse clauses from it.
```
No venv, no GPU. This is a governance and code lab.
---
## Phase 1 — Parse the provider Acceptable Use Policy (15 min)
### 1.1 Fetch and read
Choose one provider's current AUP/Terms (OpenAI or Anthropic — or both, for comparison). Read the sections covering:
- **Reverse engineering / model access** — does the ToS prohibit extracting weights, accessing the model "behind" the API, or reverse-engineering?
- **Jailbreaking / misuse** — does it prohibit attempts to bypass safety training or generate disallowed content?
- **Automated access / volume** — are there rate caps or automation restrictions?
- **Data / training opt-out** — does the provider train on API inputs by default? Is there an opt-out?
### 1.2 Extract the constraint clauses
Create `provider_tos_extract.md` and, for each relevant clause, record:
```markdown
## Clause: [short name]
- **Source**: [provider] AUP, [section/URL anchor], retrieved [date]
- **Verbatim**: "[the exact wording]"
- **Techniques constrained**: [jailbreak | weight_read | system_prompt_extract | high_volume | ...]
- **Permitted under**: [explicit permission | preview program | waiver | none]
- **Red-team implication**: [what this means for an engagement against a deployer using this model]
```
Produce at least four clauses (one per category above).
### 1.3 The key judgment
For each clause, answer: **"Can the deployer's authorization override this?"** The answer, in every case where the provider retains the right, is **no**. Record this explicitly — it is the single most important takeaway of Phase 1.
---
## Phase 2 — The AI red-team scope file (20 min)
Build `ai_scope.json` — the machine-checkable boundary that feeds B1's scope enforcement. The schema separates **deployer-controlled** from **provider-controlled** surfaces and carries a `provider_authorization` field per technique.
### 2.1 The schema
```json
{
"engagement": {
"client": "[deployer name]",
"sponsor": "[engagement sponsor]",
"valid_from": "2026-07-09T00:00:00Z",
"valid_until": "2026-08-09T00:00:00Z",
"model_versions_in_scope": ["gpt-4o-2026-05-01", "claude-opus-4-1-20260605"]
},
"deployer_surfaces": [
{
"id": "system_prompt",
"type": "config_artifact",
"data_class": "provider_only",
"retention": "engagement_plus_90d",
"techniques": ["read", "extract"]
},
{
"id": "retrieval_store",
"type": "datastore",
"data_class": "restricted",
"retention": "destroy_on_report",
"techniques": ["query", "read_metadata"]
},
{
"id": "code_exec_tool",
"type": "tool_surface",
"data_class": "public",
"retention": "indefinite",
"techniques": ["invoke", "abuse_test"]
}
],
"provider_surfaces": [
{
"id": "model_weights",
"technique": "weight_read",
"provider_authorization": {
"tos_permits": false,
"waiver_on_file": null,
"self_hosted_owned_by_deployer": false,
"decision": "BLOCK"
},
"minimum_proof": "path + hash + byte_count",
"retention": "destroy_on_report"
},
{
"id": "base_refusal_layer",
"technique": "jailbreak",
"provider_authorization": {
"tos_permits": false,
"waiver_on_file": "anthropic-research-enrollment-2026-06",
"waiver_expires": "2026-12-31T00:00:00Z",
"self_hosted_owned_by_deployer": false,
"decision": "ALLOW"
},
"measurement": "success_rate_over_N",
"disclosure_class": "provider_only_recipe"
}
],
"disclosure": {
"model_level_embargo_days": 180,
"harness_level_embargo_days": 90,
"publish_recipe_by_default": false,
"withhold_pure_misuse_no_defensive_lesson": true
}
}
```
### 2.2 Your task
- Fill in the `engagement` block with realistic values (a sample client).
- Add at least three deployer surfaces and at least two provider surfaces.
- For the `provider_surfaces`, set the `provider_authorization.decision` correctly based on the clauses you extracted in Phase 1. At least one must be `BLOCK`.
- Validate the JSON (`python3 -m json.tool ai_scope.json`).
### 2.3 The data-class decision
For each surface, justify the `data_class`:
- `public` — no sensitive data; safe to retain.
- `provider_only` — trade-secret-adjacent (system prompt); share with provider under NDA only.
- `restricted` — may contain PII (retrieval store); destroy on report.
- `destroy_on_report` — working jailbreak prompts; never carry cross-client.
Record your justification in `scope_justification.md`.
---
## Phase 3 — The CVD timeline + dual-use rubric (15 min)
Create `cvd_and_dual_use.md` with two sections.
### 3.1 CVD timeline template
A fill-in template for an AI finding's disclosure lifecycle:
```markdown
# CVD Timeline — [Finding ID]
- **Model version / checkpoint**: [pinned version]
- **Finding type**: [prompt_injection | memory_poisoning | jailbreak | weight_extraction | ...]
- **Surface level**: [model-level | harness-level]
## Timeline
- [date] — Private report to provider (NDA). Recipe shared under NDA.
- [date] — Acknowledgement from provider.
- [date] — Mitigation deployed (guardrail / version bump / harness patch).
- [date] — Residual-risk re-measurement (InjecAgent-style, success rate before/after).
- [date +180d for model-level / +90d for harness-level] — Coordinated public disclosure.
## Public advisory content
- [ ] Existence + severity: YES
- [ ] Success-rate measurement (N/M + sampling params): YES
- [ ] Defensive lesson / technique class: [YES if new | WITHHELD if pure misuse]
- [ ] Working recipe: NO (unless provider consents AND technique is independently well-known)
```
### 3.2 Dual-use decision rubric
A decision tree you run for every finding before any external communication:
```markdown
# Dual-Use Decision Rubric
1. Is the finding reported to the provider under NDA? → MUST be YES before anything else.
2. Does the finding reveal a NEW technique or architectural insight?
- YES → Publish the TECHNIQUE (not the recipe) after the embargo. This advances defense.
- NO → Go to 3.
3. Is the finding pure-misuse-enabling (a recipe with no defensive lesson)?
- YES → WITHHOLD publication. Provider-only disclosure. (Diverges from "publish everything.")
- NO → Publish existence + severity + residual-risk measurement after the embargo.
4. In all cases: never publish the working recipe unless the provider consents AND the
technique is independently well-known enough that publication adds no marginal misuse risk.
```
Write the rubric in plain language a client's counsel can sign off on.
---
## Phase 4 — The `provider_authorization_check()` gate (15 min)
Implement the function the red-team harness calls before every provider-surface technique. This is the engineering control that prevents the most common AI red-team legal mistake.
### 4.1 The spec
```python
from datetime import datetime, timezone
def provider_authorization_check(technique: str, model_id: str, scope: dict) -> dict:
"""Check whether a provider-surface technique is authorized for a target model.
Returns {"decision": "ALLOW"|"BLOCK", "reason": str}.
The deployer's authorization does NOT extend to provider-controlled surfaces;
the provider's own ToS/waiver/ownership must independently permit the technique.
"""
# 1. Find the provider_surface entry matching (technique, model_id).
# 2. Evaluate the three conditions:
# a. tos_permits == True
# b. waiver_on_file is not None AND not expired (compare to valid_until / waiver_expires)
# c. self_hosted_owned_by_deployer == True
# 3. If any condition is True -> ALLOW (record which).
# 4. Else -> BLOCK with the reason.
# 5. Always check engagement.valid_until has not passed; if expired -> BLOCK "scope expired".
pass
```
### 4.2 Implement it
Fill in the body. Edge cases to handle:
- **Expired scope** — `engagement.valid_until < now` → BLOCK with reason "engagement scope expired; re-verify authorization."
- **Expired waiver** — `waiver_expires < now` → treat `waiver_on_file` as invalid → BLOCK unless another condition holds.
- **Model not in scope** — `model_id` not in `engagement.model_versions_in_scope` → BLOCK "model version not in scope."
- **Unknown technique** — no matching `provider_surface` entry → BLOCK "technique not authorized for any provider surface."
### 4.3 Test cases
Write at least four test calls against your `ai_scope.json`:
```python
# 1. weight_read on a model with no waiver -> BLOCK
# 2. jailbreak on a model with a valid, unexpired waiver -> ALLOW
# 3. jailbreak after the waiver expires -> BLOCK
# 4. any technique after engagement.valid_until -> BLOCK
```
Print the decision + reason for each. All four must produce the correct result.
### 4.4 The point
This function is the legal control made engineering. B1's threat-model scope enforcement calls it before every provider-surface action. A red-team that runs without it is relying on a human to remember the ToS gap — and humans forget exactly when a finding is about to drop.
---
## Phase 5 — Stretch: the evidence-retention classifier (optional, 10 min)
If time remains, write a second function that classifies a captured artifact by data class and assigns its retention rule:
```python
def classify_and_retain(artifact: dict, surface: dict) -> dict:
"""Given a captured artifact and the surface it came from, return its
data_class, retention rule, and an enforceable destruction deadline."""
pass
```
The point: classification must be automatic at capture time, not a wiki entry. An evidence logger that writes everything to a permanent directory with no classification is building a compliance violation and a misuse-enabling dataset simultaneously.
---
## Deliverables
- `provider_tos_extract.md` — the clauses that constrain red-team activity (Phase 1)
- `ai_scope.json` — the machine-checkable scope boundary, surfaces separated (Phase 2)
- `scope_justification.md` — data-class justifications (Phase 2)
- `cvd_and_dual_use.md` — CVD timeline template + dual-use rubric (Phase 3)
- `provider_auth.py` — the `provider_authorization_check()` gate + test calls (Phase 4)
- (optional) `evidence_classify.py` — the retention classifier (Phase 5)
## Success criteria
- [ ] At least four provider-ToS clauses extracted, each with the "can the deployer override?" judgment.
- [ ] `ai_scope.json` validates, has ≥3 deployer surfaces and ≥2 provider surfaces, ≥1 BLOCK decision.
- [ ] CVD template distinguishes model-level (180d) from harness-level (90d) embargoes.
- [ ] Dual-use rubric reaches the "withhold pure-misuse, no-defensive-lesson" branch.
- [ ] `provider_authorization_check()` returns the correct ALLOW/BLOCK for all four test cases, including expiry.
- [ ] Every artifact ties back to a specific clause or principle from the teaching document.