File size: 10,286 Bytes
d73bfc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
"""
Response templates for the response validation task.

Each template defines what a correct API response looks like for a given
request type. The environment generates a broken response by injecting
issues (wrong status code, missing fields, wrong types, extra fields).

Response issue types:
- wrong_status_code: Response has incorrect HTTP status code
- missing_response_field: Required field missing from response body
- wrong_response_type: Field present but wrong data type
- extra_response_field: Unexpected field in response (data leak risk)
- inconsistent_error_format: Error response doesn't follow spec format
"""

import copy
import random
from typing import Any, Dict, List, Tuple

# Response issue types the agent must identify
RESPONSE_ISSUE_TYPES = [
    "wrong_status_code",
    "missing_response_field",
    "wrong_response_type",
    "extra_response_field",
    "inconsistent_error_format",
]


# Maps API operation type to expected success response
RESPONSE_TEMPLATES = [
    {
        "name": "Create Resource",
        "success_status": 201,
        "success_body": {
            "id": "res_abc123",
            "status": "created",
            "created_at": "2025-01-15T10:30:00Z",
        },
        "required_response_fields": ["id", "status", "created_at"],
        "field_types": {"id": "string", "status": "string", "created_at": "string"},
        "error_status": 400,
        "error_body": {
            "error": {"code": "VALIDATION_ERROR", "message": "Invalid input", "details": []},
        },
    },
    {
        "name": "List Resources",
        "success_status": 200,
        "success_body": {
            "data": [{"id": "res_1", "name": "Item 1"}, {"id": "res_2", "name": "Item 2"}],
            "total": 2,
            "page": 1,
            "per_page": 20,
        },
        "required_response_fields": ["data", "total", "page", "per_page"],
        "field_types": {"data": "array", "total": "integer", "page": "integer", "per_page": "integer"},
        "error_status": 401,
        "error_body": {
            "error": {"code": "UNAUTHORIZED", "message": "Invalid API key", "details": []},
        },
    },
    {
        "name": "Update Resource",
        "success_status": 200,
        "success_body": {
            "id": "res_abc123",
            "status": "updated",
            "updated_at": "2025-01-15T12:00:00Z",
        },
        "required_response_fields": ["id", "status", "updated_at"],
        "field_types": {"id": "string", "status": "string", "updated_at": "string"},
        "error_status": 404,
        "error_body": {
            "error": {"code": "NOT_FOUND", "message": "Resource not found", "details": []},
        },
    },
    {
        "name": "Delete Resource",
        "success_status": 204,
        "success_body": {},
        "required_response_fields": [],
        "field_types": {},
        "error_status": 403,
        "error_body": {
            "error": {"code": "FORBIDDEN", "message": "Insufficient permissions", "details": []},
        },
    },
    {
        "name": "Batch Operation",
        "success_status": 200,
        "success_body": {
            "processed": 5,
            "failed": 0,
            "results": [
                {"id": "item_1", "status": "success"},
                {"id": "item_2", "status": "success"},
            ],
        },
        "required_response_fields": ["processed", "failed", "results"],
        "field_types": {"processed": "integer", "failed": "integer", "results": "array"},
        "error_status": 422,
        "error_body": {
            "error": {"code": "UNPROCESSABLE", "message": "Batch validation failed", "details": []},
        },
    },
    {
        "name": "Authentication",
        "success_status": 200,
        "success_body": {
            "access_token": "eyJhbGciOiJIUzI1NiJ9.token",
            "token_type": "Bearer",
            "expires_in": 3600,
        },
        "required_response_fields": ["access_token", "token_type", "expires_in"],
        "field_types": {"access_token": "string", "token_type": "string", "expires_in": "integer"},
        "error_status": 401,
        "error_body": {
            "error": {"code": "INVALID_CREDENTIALS", "message": "Bad credentials", "details": []},
        },
    },
    {
        "name": "File Upload",
        "success_status": 201,
        "success_body": {
            "file_id": "file_xyz789",
            "filename": "report.pdf",
            "size_bytes": 1048576,
            "url": "https://cdn.example.com/files/file_xyz789",
        },
        "required_response_fields": ["file_id", "filename", "size_bytes", "url"],
        "field_types": {"file_id": "string", "filename": "string", "size_bytes": "integer", "url": "string"},
        "error_status": 413,
        "error_body": {
            "error": {"code": "PAYLOAD_TOO_LARGE", "message": "File exceeds 10MB limit", "details": []},
        },
    },
    {
        "name": "Search Query",
        "success_status": 200,
        "success_body": {
            "query": "test search",
            "results": [{"id": "doc_1", "score": 0.95, "title": "Test Document"}],
            "total_results": 1,
            "search_time_ms": 42,
        },
        "required_response_fields": ["query", "results", "total_results", "search_time_ms"],
        "field_types": {"query": "string", "results": "array", "total_results": "integer", "search_time_ms": "integer"},
        "error_status": 400,
        "error_body": {
            "error": {"code": "INVALID_QUERY", "message": "Query syntax error", "details": []},
        },
    },
]


def get_random_response_template(rng: random.Random) -> Dict[str, Any]:
    """Pick a random response template."""
    return copy.deepcopy(rng.choice(RESPONSE_TEMPLATES))


def inject_response_issues(
    template: Dict[str, Any],
    rng: random.Random,
    issue_count: int = 1,
) -> Tuple[Dict[str, Any], int, List[Dict[str, str]]]:
    """Inject issues into a response and return (broken_body, broken_status, ground_truths).

    Each ground truth has: issue_type, description, affected_field (if applicable).
    """
    # Decide if we break a success response or an error response
    use_success = rng.random() < 0.6
    if use_success:
        body = copy.deepcopy(template["success_body"])
        status = template["success_status"]
    else:
        body = copy.deepcopy(template["error_body"])
        status = template["error_status"]

    ground_truths: List[Dict[str, str]] = []
    available_issues = list(RESPONSE_ISSUE_TYPES)
    rng.shuffle(available_issues)

    injected = 0
    for issue_type in available_issues:
        if injected >= issue_count:
            break

        if issue_type == "wrong_status_code":
            wrong_codes = [200, 201, 204, 301, 400, 401, 403, 404, 422, 429, 500, 502, 503]
            wrong_codes = [c for c in wrong_codes if c != status]
            old_status = status
            status = rng.choice(wrong_codes)
            ground_truths.append({
                "issue_type": "wrong_status_code",
                "description": f"Expected status {old_status}, got {status}",
                "affected_field": "status_code",
                "correct_value": str(old_status),
            })
            injected += 1

        elif issue_type == "missing_response_field" and template["required_response_fields"]:
            fields = list(template["required_response_fields"])
            rng.shuffle(fields)
            field = fields[0]
            if field in body:
                del body[field]
                ground_truths.append({
                    "issue_type": "missing_response_field",
                    "description": f"Required field '{field}' missing from response",
                    "affected_field": field,
                })
                injected += 1

        elif issue_type == "wrong_response_type" and template["field_types"]:
            typed_fields = [f for f in template["field_types"] if f in body]
            if typed_fields:
                field = rng.choice(typed_fields)
                original_type = template["field_types"][field]
                # Replace with wrong type
                if original_type == "string":
                    body[field] = 12345
                elif original_type == "integer":
                    body[field] = "not_a_number"
                elif original_type == "array":
                    body[field] = "should_be_array"
                else:
                    body[field] = [1, 2, 3]
                ground_truths.append({
                    "issue_type": "wrong_response_type",
                    "description": f"Field '{field}' should be {original_type}",
                    "affected_field": field,
                })
                injected += 1

        elif issue_type == "extra_response_field":
            leak_fields = {
                "internal_id": "int_9f8a2b",
                "debug_trace": "stack trace at line 42",
                "db_query": "SELECT * FROM users WHERE id=123",
                "server_ip": "10.0.0.42",
                "session_token": "sess_leaked_abc",
            }
            field, value = rng.choice(list(leak_fields.items()))
            body[field] = value
            ground_truths.append({
                "issue_type": "extra_response_field",
                "description": f"Unexpected field '{field}' in response (potential data leak)",
                "affected_field": field,
            })
            injected += 1

        elif issue_type == "inconsistent_error_format" and not use_success:
            # Break the error format -- flatten it or use wrong keys
            variants = [
                {"msg": body.get("error", {}).get("message", "error"), "err_code": "UNKNOWN"},
                {"error_message": "Something went wrong", "status": "error"},
                {"errors": [{"msg": "bad request"}]},
            ]
            body = rng.choice(variants)
            ground_truths.append({
                "issue_type": "inconsistent_error_format",
                "description": "Error response doesn't follow standard format: {error: {code, message, details}}",
                "affected_field": "error",
            })
            injected += 1

    return body, status, ground_truths