Skip to content

Commit 09ca357

Browse files
committed
datasets: define the agent-trace schema, validator and format docs
agentdataflow_toolexchange_traces/ has been open for contributions with a TODO where its data format should be. This defines the format and ships the tooling that enforces it. Entries follow in a separate PR, so the format can be reviewed without the data riding along. provenance.tier records how a trace was obtained - sanitized production, lab or CTF, derived from a documented public fact, or hypothetical - because the people who hold real agent traces mostly cannot publish them. Every tier above hypothetical requires evidence a reader can check without the contributor, enforced by validate.py, so the kinds of contribution never pretend to be one another. A contributor-controlled source does not establish a precondition at any tier; that rule cannot be checked mechanically and is stated as a review-time obligation in the README and schema, not implied to be automated. The category enum matches the values already used by the repo's central data_validation/schemas/agentdataflow_trace.schema.json. Where this schema diverges from that one, the divergence is deliberate and set out in the pull request description. validate.py checks schema conformance, the span graph (including that a span cannot name itself or a later span as its parent), citation form per evidence type, and ISO dates on date_added/date_observed and every span timestamp in every environment, and scans every string in an entry - not only span payloads - for credential-, secret-, and internal-hostname-shaped values and for clusters of addresses that look like real routable public IPv4 addresses, independently of the contributor's sanitization attestation. It passes with entries/ empty, validating example.json on its own. A GitHub Actions workflow runs validate.py and checks index.csv is current, path-filtered to this dataset, so the checks above run on every PR rather than depending on a contributor having run them locally.
1 parent 2aa707a commit 09ca357

6 files changed

Lines changed: 1248 additions & 18 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: dataset-agentdataflow-validate
2+
3+
# Runs the dataset-local validator for agentdataflow_toolexchange_traces on
4+
# every change to it, so "enforced by validate.py" in that dataset's README
5+
# is true of the merged tree and not only of whoever remembered to run it
6+
# locally. Path-filtered to the dataset, same pattern as scanner-selftest.
7+
on:
8+
push:
9+
branches: [main]
10+
paths:
11+
- 'datasets/agentdataflow_toolexchange_traces/**'
12+
- 'datasets/_shared/**'
13+
- '.github/workflows/dataset-agentdataflow-validate.yml'
14+
pull_request:
15+
paths:
16+
- 'datasets/agentdataflow_toolexchange_traces/**'
17+
- 'datasets/_shared/**'
18+
- '.github/workflows/dataset-agentdataflow-validate.yml'
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
validate:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
32+
with:
33+
python-version: '3.11'
34+
35+
- name: Install jsonschema
36+
run: python -m pip install --quiet jsonschema
37+
38+
- name: Validate entries and example against schema.json
39+
run: python validate.py
40+
working-directory: datasets/agentdataflow_toolexchange_traces
41+
42+
- name: index.csv is up to date with entries/
43+
run: |
44+
python build_index.py
45+
git diff --exit-code index.csv
46+
working-directory: datasets/agentdataflow_toolexchange_traces

‎datasets/agentdataflow_toolexchange_traces/README.md‎

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,113 @@ Structured traces capturing how data flows through agentic AI systems — betwee
1010

1111
Categories include:
1212

13-
- **Tool call traces** — Sequences of tool invocations showing what data is sent to and received from external tools (APIs, databases, file systems, code execution environments)
14-
- **Multi-agent delegation chains** — Traces showing how tasks and data are passed between agents, including what context is forwarded, filtered, or accumulated
15-
- **Plugin data exchange logs** — Records of data shared with third-party plugins, including request/response payloads and metadata
16-
- **Credential and permission flows** — How credentials, tokens, and permissions are scoped, delegated, and consumed across agent workflows
17-
- **Context accumulation patterns** — Traces showing how agent context windows grow over multi-step tasks, what data persists across steps, and where over-sharing occurs
18-
- **Memory read/write traces** — How agents interact with short-term and long-term memory stores, including what data is persisted and retrieved
13+
- **Tool call traces** (`tool_call`) — Sequences of tool invocations showing what data is sent to and received from external tools (APIs, databases, file systems, code execution environments)
14+
- **Multi-agent delegation chains** (`multi_agent_delegation`) — Traces showing how tasks and data are passed between agents, including what context is forwarded, filtered, or accumulated
15+
- **Plugin data exchange logs** (`plugin_data_exchange`) — Records of data shared with third-party plugins, including request/response payloads and metadata
16+
- **Credential and permission flows** (`credential_flow`) — How credentials, tokens, and permissions are scoped, delegated, and consumed across agent workflows
17+
- **Context accumulation patterns** (`context_accumulation`) — Traces showing how agent context windows grow over multi-step tasks, what data persists across steps, and where over-sharing occurs
18+
- **Memory read/write traces** (`memory_read_write`) — How agents interact with short-term and long-term memory stores, including what data is persisted and retrieved
19+
20+
## Files
21+
22+
| File | Purpose |
23+
|---|---|
24+
| `schema.json` | JSON Schema (draft 2020-12) every entry validates against |
25+
| `example.json` | Worked example. Read it with the schema before writing an entry |
26+
| `entries/` | One trace per file, named `<trace_id>.json`. Empty until the first entry merges; `validate.py` passes against `example.json` alone in the meantime. |
27+
| `validate.py` | `python validate.py` - schema, provenance, span graph, and secret-scan checks |
28+
| `build_index.py` | `python build_index.py` - regenerates `index.csv` |
29+
| `index.csv` | Flat index of all entries, for filtering and citation. Generated once `entries/` is non-empty. |
1930

2031
## Data Format
2132

22-
<!-- TODO: Define schema once initial data is contributed -->
33+
One trace per file in `entries/`, named after its `trace_id` (`DSGAI-TRACE-<slug>.json`). The authoritative definition is `schema.json`; the summary below is orientation.
2334

24-
Contributions should include:
35+
Every entry carries:
2536

26-
- **Trace ID**
27-
- **Category** — From the list above
28-
- **DSGAI mapping** — Primary DSGAI entries relevant to this trace
29-
- **Agent framework** — LangGraph, AutoGPT, CrewAI, custom, etc. (if disclosable)
30-
- **Trace data** — The sequence of events, tool calls, and data exchanges in structured format (JSON, JSONL, or OpenTelemetry-compatible spans)
31-
- **Data sensitivity annotations** — Flag any steps where sensitive data is present, over-shared, or inadequately scoped
32-
- **Security observations** — What data security risks this trace illustrates
33-
- **Benign / adversarial** — Whether this is a normal workflow trace or one demonstrating a security failure
37+
- **`trace_id`, `title`, `description`** - what flows, and what makes it security-relevant
38+
- **`category`** - one of the six above
39+
- **`disposition`** - `benign`, `adversarial`, or `unintentional_failure`
40+
- **`dsgai_mapping`** - DSGAI entries this trace illustrates, primary risk first
41+
- **`provenance`** - where the trace came from, and what backs it (see below)
42+
- **`sanitization`** - a hard attestation plus the techniques applied
43+
- **`spans`** - the trace itself: an ordered sequence of events
44+
- **`security_observations`** - what the trace demonstrates, one falsifiable claim per item
45+
46+
Optional: `agent` (framework, protocol, topology, autonomy), `owasp_llm_top10_mapping`, `mitre_atlas_mapping`, `mitigations`, `contributor`, `tags`, `notes`.
47+
48+
### Spans
49+
50+
A span is one event. The shape is OpenTelemetry-compatible without requiring an OTel pipeline to produce it, so a trace can be exported from a real system or written by hand and still validate the same way.
51+
52+
```json
53+
{
54+
"span_id": "s7",
55+
"parent_span_id": "s6",
56+
"t_offset_ms": 176400910,
57+
"actor": "tool",
58+
"actor_id": "tool://server-b/lookup_record",
59+
"operation": "tool.result",
60+
"summary": "Tool returns the full record, three fields wider than the schema the agent bound to.",
61+
"payload": { "contact_email": "<synthetic:email>", "internal_notes": "<shape:string,len=1840>" },
62+
"data_classes": ["tool_output", "pii"],
63+
"sensitivity": "high",
64+
"finding": {
65+
"dsgai_id": "DSGAI01",
66+
"note": "Entitlement was enforced once, against a schema that has since changed.",
67+
"severity": "High"
68+
}
69+
}
70+
```
71+
72+
Two conventions carry most of the weight:
73+
74+
**Payloads are shapes, not content.** String values in `payload` should be typed placeholders - `<synthetic:email>`, `<redacted:bearer-token>`, `<shape:string,len=1840>`. Literal content belongs in a trace only where the literal *is* the published artifact, such as a prompt-injection string from a paper. This is not only a privacy rule: it is what makes traces from real systems shareable at all, because the finding almost always lives in the shape.
75+
76+
**Findings hang off spans.** Attaching a risk to the span where it materializes lets a reader see exactly where a flow stops being safe, instead of inferring it from prose. `validate.py` requires every span-level `finding.dsgai_id` to appear in the trace's top-level `dsgai_mapping`.
77+
78+
Prefer `t_offset_ms` over wall-clock timestamps. Relative offsets carry the ordering that matters without inventing precision or disclosing when a system was running. Use `timestamp` only where the absolute date is part of the finding.
79+
80+
### Provenance tiers
81+
82+
Most people who hold real agent traces cannot publish them, because production telemetry contains everything that must not be published. A dataset that accepts only production telemetry stays empty; a dataset that accepts anything fills with plausible fiction. The `provenance.tier` field is how this dataset takes both contributions without confusing them:
83+
84+
| Tier | Meaning |
85+
|---|---|
86+
| `observed_production` | Sanitized telemetry from a real deployed system |
87+
| `observed_lab` | Sandbox, test environment, CTF, or red-team exercise |
88+
| `derived_from_public_documentation` | The mechanism is rendered as a trace, and its precondition is a documented public fact carried in `evidence` - a specification, an advisory, a CVE, third-party research. The source may be normative (what a protocol permits) or empirical (what was observed); either is fine, as long as it is checkable without the contributor. |
89+
| `hypothetical` | Illustrative only, no empirical backing |
90+
91+
Every tier except `hypothetical` requires at least one `provenance.evidence` item, enforced by `validate.py`, and every item has to say what it supports, narrowly. Acceptable backing is anything a reader can check without the contributor: a normative statement in a protocol specification, a published advisory or CVE, a vendor changelog, third-party research, a DOI. A source the contributor owns or controls - their own site, their own dataset, their own publication - does not establish a precondition here, whatever it says. This rule is not machine-checkable and is not enforced by `validate.py`; it is a review-time obligation, the same as the sanitization judgment call a reviewer makes on every entry. Where a contributor-controlled source is the only backing available, the honest tier is `hypothetical`. Statistics computed over this dataset should be reported per tier, never pooled across them.
92+
93+
Choose the tier honestly. `derived_from_public_documentation` is not a lesser contribution; it is the tier that lets a documented phenomenon be studied as a data flow without anyone publishing their logs. A specification saying what a protocol permits and telemetry saying what happened are different claims with different weight, which is exactly why the field is named "documentation" and not "observation."
94+
95+
### MITRE ATLAS mappings
96+
97+
`mitre_atlas_mapping` is release-pinned by construction: an entry records the ATLAS release it was verified against and the name each identifier carried in that release. ATLAS identifiers and their names have both moved between releases, so an unpinned pair silently rots. Omit the property rather than guess.
3498

3599
## Sanitization Requirements
36100

37101
All traces **must** be sanitized before submission:
38102

39-
- No real API keys, tokens, credentials, or secrets — replace with placeholder values (e.g., `sk-REDACTED`, `Bearer EXAMPLE_TOKEN`)
103+
- No real API keys, tokens, credentials, or secrets — replace with placeholder values (e.g. `<redacted:bearer-token>`)
40104
- No real PII, PHI, or proprietary data — use synthetic equivalents
41105
- No internal hostnames, IP addresses, or infrastructure details
42106
- Generalize organization-specific tool names if they could identify the source
43107

44108
Traces from test environments, sandboxes, or CTF exercises are ideal. Production traces must be thoroughly sanitized.
45109

110+
Two things back this up rather than leaving it to good intentions. `sanitization.attestation` must be a literal `true`, so an entry cannot merge with the box quietly unticked. And `validate.py` scans every string in the entry, independently of that attestation: payloads, summaries, notes, evidence locators. Placeholders are exempt; values matching one of its patterns fail the run - common credential shapes, RFC1918 addresses, `.internal`/`.corp`/`.lan`/`.intra` hostnames, home directory paths, credential-shaped `key=value` assignments, and two or more addresses in one string that look like real routable public IPv4 addresses. The scan is deliberately wider than the payloads, because prose written while looking at a real trace is where a real hostname actually gets typed. It is pattern matching, not a guarantee: a contributor is still the one certifying `sanitization.attestation`, and a reviewer is still the backstop for anything the patterns miss.
111+
112+
Naming a third party as the subject of a security failure is a disclosure act, not a dataset contribution. Generalize the counterparty unless you have gone through disclosure and are prepared to say so in `notes`.
113+
46114
## Contributing
47115

48-
Add traces as individual JSON, JSONL, or YAML files and submit a pull request. See the [main datasets README](../README.md) for general contribution guidelines.
116+
1. Copy `example.json` and edit it, or export spans from your own system into the same shape
117+
2. Save as `entries/<trace_id>.json`
118+
3. Run `python validate.py` - it must print OK
119+
4. Run `python build_index.py` to refresh `index.csv`
120+
5. Open a pull request describing what the trace demonstrates and which DSGAI entries it maps to
121+
122+
See the [main datasets README](../README.md) for general contribution guidelines, and `#team-genai-data-security-initiative` on the [OWASP Slack workspace](https://owasp.slack.com) for anything that needs a conversation first.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Generate index.csv from every entry in ./entries/.
2+
3+
Usage: python build_index.py
4+
Writes ./index.csv with key flat columns; arrays are joined with '|'.
5+
Stdlib only.
6+
"""
7+
from __future__ import annotations
8+
9+
import csv
10+
import json
11+
from pathlib import Path
12+
13+
ROOT = Path(__file__).resolve().parent
14+
ENTRIES_DIR = ROOT / "entries"
15+
INDEX_PATH = ROOT / "index.csv"
16+
17+
SENSITIVITY_ORDER = ["none", "low", "moderate", "high"]
18+
19+
COLUMNS = [
20+
"trace_id",
21+
"title",
22+
"category",
23+
"disposition",
24+
"provenance_tier",
25+
"primary_dsgai",
26+
"dsgai_mapping",
27+
"owasp_llm_top10_mapping",
28+
"agent_framework",
29+
"tool_protocol",
30+
"span_count",
31+
"finding_count",
32+
"max_sensitivity",
33+
"data_classes",
34+
"evidence_count",
35+
"primary_evidence",
36+
"date_added",
37+
"tags",
38+
]
39+
40+
41+
def max_sensitivity(spans: list[dict]) -> str:
42+
best = -1
43+
for span in spans:
44+
value = span.get("sensitivity")
45+
if value in SENSITIVITY_ORDER:
46+
best = max(best, SENSITIVITY_ORDER.index(value))
47+
return SENSITIVITY_ORDER[best] if best >= 0 else ""
48+
49+
50+
def row_for(entry: dict) -> dict[str, str]:
51+
spans = entry.get("spans", []) or []
52+
prov = entry.get("provenance", {}) or {}
53+
agent = entry.get("agent", {}) or {}
54+
evidence = prov.get("evidence", []) or []
55+
dsgai = entry.get("dsgai_mapping", []) or []
56+
57+
data_classes: list[str] = []
58+
for span in spans:
59+
for cls in span.get("data_classes", []) or []:
60+
if cls not in data_classes:
61+
data_classes.append(cls)
62+
63+
return {
64+
"trace_id": entry.get("trace_id", ""),
65+
"title": entry.get("title", ""),
66+
"category": entry.get("category", ""),
67+
"disposition": entry.get("disposition", ""),
68+
"provenance_tier": prov.get("tier", ""),
69+
"primary_dsgai": dsgai[0] if dsgai else "",
70+
"dsgai_mapping": "|".join(dsgai),
71+
"owasp_llm_top10_mapping": "|".join(entry.get("owasp_llm_top10_mapping", []) or []),
72+
"agent_framework": agent.get("framework", ""),
73+
"tool_protocol": agent.get("tool_protocol", ""),
74+
"span_count": str(len(spans)),
75+
"finding_count": str(sum(1 for s in spans if s.get("finding"))),
76+
"max_sensitivity": max_sensitivity(spans),
77+
"data_classes": "|".join(sorted(data_classes)),
78+
"evidence_count": str(len(evidence)),
79+
"primary_evidence": evidence[0].get("citation", "") if evidence else "",
80+
"date_added": entry.get("date_added", ""),
81+
"tags": "|".join(entry.get("tags", []) or []),
82+
}
83+
84+
85+
def main() -> None:
86+
rows = []
87+
for path in sorted(ENTRIES_DIR.glob("*.json")):
88+
entry = json.loads(path.read_text(encoding="utf-8"))
89+
rows.append(row_for(entry))
90+
91+
with INDEX_PATH.open("w", newline="", encoding="utf-8") as f:
92+
writer = csv.DictWriter(f, fieldnames=COLUMNS)
93+
writer.writeheader()
94+
writer.writerows(rows)
95+
96+
print(f"Wrote {INDEX_PATH.name} with {len(rows)} entries.")
97+
98+
99+
if __name__ == "__main__":
100+
main()

0 commit comments

Comments
 (0)