# API schema changes: सुरक्षित agents के लिए contract tests

कोई agent किसी छोटे API बदलाव को human-operated client की तुलना में कहीं जल्दी वास्तविक action error में बदल सकता है। Response field का नाम बदलने पर वह एक resource के बजाय हर resource चुन सकता है। नया default query का दायरा बढ़ा सकता है। बदला हुआ status object retry की अनुमति जैसा लग सकता है, और retry charge, deployment या deletion request को दोहरा सकता है।

खतरा यह है कि ऐसी failures conventional monitoring में अक्सर healthy दिखती हैं। Provider HTTP 200 लौटाता है। Client crash नहीं करता। Agent एक plausible explanation देता है। Contract tests को केवल यह नहीं जांचना चाहिए कि JSON parse होता है, बल्कि यह भी कि agent API data का क्या अर्थ निकालता है।

## Agent decisions compatibility को safety property बनाते हैं

किसी व्यक्ति द्वारा इस्तेमाल किया गया API client response बदलने पर अक्सर साफ तौर पर fail होता है। Button खाली table दिखाता है, form error दिखाई देता है और अगला action लेने से पहले कोई जांच कर लेता है। Agents अक्सर response को सीधे दूसरी request में बदल देते हैं। किसी transcript के दिखने से पहले वे यह conversion कई बार कर सकते हैं।

मान लीजिए एक cleanup agent `GET /projects?state=inactive` call करता है, हर item का `owner` field पढ़ता है और allowlist से बाहर के projects को archive करने से पहले approval मांगता है। बाद में provider पुराने endpoint और status code को रखते हुए `owner` को `owner_id` में बदल देता है। Loose parser missing `owner` को empty string में बदल देता है। अगर agent का rule empty owner को unassigned मानता है, तो वह कहीं बड़े archive request की तैयारी करेगा।

यह authentication failure या prompt failure नहीं है। यह API boundary पर interpretation failure है। Corrective control भी इसी boundary पर होना चाहिए।

इन decisions को प्रभावित करने वाले हर field को safety contract का हिस्सा मानें:

- agent किस object पर action ले सकता है
- कोई object action के योग्य है या नहीं
- action का scope, amount या destination
- पिछला action पूरा हुआ, fail हुआ या retry चाहिए
- आगे बढ़ने से पहले agent को किसी व्यक्ति से पूछना चाहिए या नहीं

यह अंतर महत्वपूर्ण है, क्योंकि transport compatibility behavioral compatibility से बहुत कमजोर होती है। कोई service अपना endpoint, method, authentication scheme और JSON syntax बनाए रखकर भी response के बाद होने वाले decision को तोड़ सकती है। Teams अक्सर इसे nonbreaking change कहते हैं, क्योंकि मौजूदा SDK अभी भी इसे deserialize कर लेते हैं। Autonomous caller के लिए यह label खतरनाक रूप से अधूरा हो सकता है।

हर उस response value का छोटा map बनाएं जो action selector, authorization decision या retry branch तक पहुंचती है। किसी विशाल API specification की हर property से शुरुआत न करें। उन values से शुरू करें जिनकी गलत interpretation agent के काम करने की क्षमता बदल देती है।

## Schema diff behavioral compatibility साबित नहीं कर सकता

Schema diff tools उपयोगी बदलाव पकड़ते हैं, लेकिन यह नहीं बता सकते कि कोई बदलाव किसी खास agent के लिए सुरक्षित है या नहीं। वे declarations की तुलना करते हैं। Agent ऐसे semantics पर निर्भर करता है जो अक्सर उन declarations के बाहर होते हैं।

मान लीजिए provider `limit` को optional parameter से बदलकर ऐसा optional parameter कर देता है जिसका implicit default 100 के बजाय 1000 है। Standard OpenAPI diff में required property का कोई बदलाव नहीं दिख सकता। फिर भी `limit` छोड़ने वाला agent अब दस गुना objects देख सकता है और उन सभी पर batch action भेज सकता है।

OpenAPI Specification Schema Object को JSON Schema vocabulary का superset बताती है और कहती है कि उसकी properties request और response payloads की information देती हैं। यह documentation और validation के लिए उपयोगी है। लेकिन यह नहीं बताती कि `state: "pending"` retry की अनुमति देता है या omitted `limit` 100 पर capped रहेगा। ये workflow assertions हैं और tests में इन्हें plain terms में लिखना होगा।

इसी तरह JSON Schema का `default` annotation validator या client को value डालने के लिए मजबूर नहीं करता। कई developers ऐसा मान लेते हैं। JSON Schema documentation `default` को annotation data मानती है, instance बदलने वाला command नहीं। अगर safety किसी value पर निर्भर है, तो client से उसे explicitly भेजवाएं और exact outbound request test करें। Omission को schema annotation से ठीक होने की उम्मीद न रखें।

Diff tool को alarm bell की तरह इस्तेमाल करें। फिर हर reported change को उस action path के हिसाब से classify करें जिस पर उसका असर पड़ सकता है:

- Renamed identifier चुने गए target को बदल सकता है।
- Enum addition parser को untested branch में भेज सकता है।
- Default change request code बदले बिना scope बदल सकता है।
- Representation change completion या failure का अर्थ उलट सकता है।

उलटा भी महत्वपूर्ण है। Diff tool किसी ऐसे descriptive field को report कर सकता है जिसे कोई agent पढ़ता ही नहीं। उसकी review जरूरी है, लेकिन production freeze नहीं। Compatibility review तब बेहतर होती है जब वह हर schema line को समान जोखिम मानने के बजाय data को decision तक follow करती है।

## Contract tests को requests और decisions दोनों pin करने चाहिए

एक उपयोगी contract test के दो हिस्से होते हैं: वह agent द्वारा भेजी गई वास्तविक request verify करता है, फिर provider response पढ़ने के बाद agent के proposed action को verify करता है। केवल एक हिस्से की testing बड़ा blind spot छोड़ देती है।

Defaults के लिए local test server या provider sandbox पर वास्तविक HTTP request capture करें। नीचे दिया Python example outgoing request inspect करने के लिए `httpx.MockTransport` का इस्तेमाल करता है। यह उस आम failure को रोकता है जिसमें client destructive operation के लिए provider default पर चुपचाप निर्भर रहता है।

```python
import httpx

seen = []

def handler(request: httpx.Request) -> httpx.Response:
    seen.append({
        "method": request.method,
        "path": request.url.path,
        "query": dict(request.url.params),
    })
    return httpx.Response(200, json={"items": []})

client = httpx.Client(transport=httpx.MockTransport(handler))

response = client.get(
    "https://api.example.test/projects",
    params={"state": "inactive", "limit": "100"},
)

assert response.status_code == 200
assert seen == [{
    "method": "GET",
    "path": "/projects",
    "query": {"state": "inactive", "limit": "100"},
}]
```

महत्वपूर्ण assertion 200 response नहीं, बल्कि explicit `limit` है। अगर refactor यह parameter हटा देता है, तो provider का नया default selection को बड़ा करने से पहले test fail हो जाएगा।

इसके बाद decision test करें। Agent के planning function को HTTP call करने वाले code से अलग रखें, ताकि test कोई action किए बिना proposed action inspect कर सके।

```python
from dataclasses import dataclass

@dataclass
class ArchivePlan:
    project_ids: list[str]
    requires_approval: bool

def plan_archives(items: list[dict], allowed_owners: set[str]) -> ArchivePlan:
    targets = []
    for item in items:
        owner = item.get("owner")
        if owner is None:
            raise ValueError("provider response lacks owner")
        if item["state"] == "inactive" and owner in allowed_owners:
            targets.append(item["id"])
    return ArchivePlan(targets, requires_approval=bool(targets))

items = [
    {"id": "p17", "state": "inactive", "owner": "team-a"},
    {"id": "p18", "state": "inactive", "owner": "team-b"},
]

plan = plan_archives(items, {"team-a"})
assert plan.project_ids == ["p17"]
assert plan.requires_approval is True
```

यह test वह choice स्पष्ट करता है जिसे loose code अक्सर टाल देता है: missing `owner` पर error उठना चाहिए। Action selection field के लिए fail closed करें। Empty string, `None` या guessed fallback लौटाने से process चलता रह सकता है, लेकिन detectable integration failure की जगह संभवतः unsafe plan बन जाता है।

Fixtures इतने छोटे रखें कि reviewer देख सके कि हर item क्यों मौजूद है। हजार objects वाला fixture production जैसा लग सकता है, पर वह उस condition को छिपा देता है जिसे आप सुरक्षित रखना चाहते थे।

## Renamed fields के लिए explicit failure behavior चाहिए

Renamed field को action path रोक देना चाहिए, जब तक कि defined migration के दौरान दोनों names को जानबूझकर support न किया जा रहा हो। Silent fallback handling demo में resilient लगती है, लेकिन production में unreviewed semantics बनाती है।

सबसे खराब रूप कुछ ऐसा दिखता है:

```python
owner = item.get("owner", "")
if owner not in blocked_owners:
    archive(item["id"])
```

जब `owner` गायब हो जाता है, तो हर item ऐसा लगता है जैसे उसका owner blocked नहीं है। Parser ने वही किया जो code ने कहा था। इसे लिखने वाला engineer शायद `KeyError` से बचना चाहता था। इस छोटी सुविधा ने data की अनुपस्थिति को action की permission में बदल दिया।

तीनों स्थितियों के लिए tests लिखें: expected field, पुराना field अगर temporary compatibility promise है, और दोनों में से कोई field नहीं। तीसरा test बताए कि agent रुकता है, object skip करता है या clarification मांगता है। Target identity, authorization state और action scope के लिए रुकना आमतौर पर सही उत्तर है।

अगर alias support करते हैं, तो उसकी precedence स्पष्ट और अस्थायी रखें:

```python
def read_owner(item: dict) -> str:
    if "owner" in item:
        return item["owner"]
    if "owner_id" in item:
        return item["owner_id"]
    raise ValueError("owner identity missing")
```

इस code को contradictory data का test भी चाहिए। अगर दोनों fields आएं और अलग-अलग values हों, तो किसी एक को चुपचाप प्राथमिकता न दें। Error उठाएं और provider से ambiguity दूर करने को कहें। Compatibility layer को ज्ञात पुराने अर्थ को बनाए रखना चाहिए, असंगत records के लिए tie breaker नहीं गढ़ना चाहिए।

Teams कभी-कभी कहते हैं कि permissive parsing provider evolution से बचाती है। Unknown fields को ignore करने पर यह harmless additions से बचा सकती है। लेकिन action नियंत्रित करने वाले missing fields से नहीं। यहां उलटा behavior चाहिए: अतिरिक्त information स्वीकार करें, पर आवश्यक meaning अनुपस्थित हो तो reject करें।

## Defaults और omission के लिए अलग-अलग tests चाहिए

Omitted property, explicit null और explicit value तीन अलग requests हैं। General purpose serializers इन्हें धुंधला कर देते हैं, इसलिए agents भी अक्सर ऐसा करते हैं।

Request builder अपने internal value `None` होने पर `dry_run` omit कर सकता है। Provider omission को `false` मान सकता है। बाद में provider release omission का अर्थ «account setting इस्तेमाल करें» कर सकती है, और किसी tenant की account setting false तथा किसी दूसरे की true हो सकती है। Agent code नहीं बदला, लेकिन action बदल गया।

Decision को प्रभावित करने वाले options को दो categories में रखें। जहां safe behavior ज्ञात है, value हर बार भेजें। जहां operator choice चाहिए, request बनाने से पहले choice अनिवार्य करें। Destructive या externally visible actions में «let the server decide» नाम की तीसरी category न रखें।

Exact cases की table के साथ serialization test करें। उद्देश्य serialization से पहले language object देखना नहीं, बल्कि wire representation inspect करना है।

| Intent | Outbound representation | Expected provider meaning |
| --- | --- | --- |
| Inactive projects पढ़ना | `state=inactive&limit=100` | सीमित selection |
| Archive का simulation | `{"dry_run": true}` | कोई archive नहीं होगा |
| एक project archive करना | `{"project_ids":["p17"],"dry_run": false}` | केवल p17 बदल सकता है |
| Mode के लिए operator choice नहीं | Request locally reject होती है | Provider को कुछ नहीं मिलता |

Pagination के साथ भी उतने ही strict रहें। `next_cursor` जोड़ने वाला response agent को हर page अपने आप fetch करने के लिए प्रेरित कर सकता है। Report के लिए यह उचित और action planner के लिए लापरवाही हो सकती है। Planner कितने अधिकतम objects देख सकता है और second page की अनुमति किस condition में है, दोनों test करें। Cursor continuation mechanism है, unlimited scope की consent नहीं।

Provider defaults responses में भी मायने रखते हैं। अगर API `archivable` को false होने पर omit करना शुरू कर दे, तो `if item.get("archivable", True)` जैसा code unsafe दिशा में behavior बदल देगा। Permission देने वाले field के लिए `item.get("archivable") is True` जैसा explicit comparison करें। यह कम elegant है, लेकिन audit करना बहुत आसान है।

## Response validation को केवल shape नहीं, meaning बचाना चाहिए

Response validation को malformed data और unfamiliar लेकिन harmless data में अंतर करना चाहिए। Blanket strictness provider के fields जोड़ने पर clients तोड़ देती है। Blanket permissiveness missing evidence को guess में बदल देती है।

अगले action को feed करने वाली values के आसपास narrow response model बनाएं। हर value के लिए type, allowed states और absence पर workflow रुकता है या नहीं, यह तय करें। Project identifier के लिए केवल `string` पर्याप्त नहीं; agent को nonempty stable identifier चाहिए जो बाद में archive request में भेजे गए identifier से match करे। Status के लिए भी केवल `string` पर्याप्त नहीं; हर enumerated state के लिए documented action चाहिए।

उदाहरण के लिए यह parser बदली हुई status representation संभालते हुए खुद को retry की permission नहीं देता:

```python
ALLOWED_STATES = {"queued", "running", "succeeded", "failed"}

def retryable(job: dict) -> bool:
    status = job.get("status")
    if status not in ALLOWED_STATES:
        raise ValueError(f"unknown job status: {status!r}")
    return status == "failed" and job.get("retry_allowed") is True
```

अगर provider `status` को string से बदलकर `{ "phase": "failed" }` जैसे object में बदल दे, तो यह code रुक जाएगा। जब तक कोई तय न करे कि नई representation पुराने workflow में कैसे map होती है, यह interruption सही है। Provider `cancelled` जोड़ दे, तब भी रुकना सही है, जब तक team तय न करे कि cancellation terminal है, retryable है या व्यक्ति की जरूरत है।

Read-only display के लिए हर unknown enum value को emergency न बनाएं। Consequence action के अनुरूप होनी चाहिए। Reporting agent unfamiliar state को label करके आगे बढ़ सकता है। Billing job retry करने या resource delete करने वाले agent को अपरिचित state पर action से पहले रुकना चाहिए।

Fields के बीच relationships भी test करें। Response structurally valid हो सकता है, फिर भी `status: "succeeded"` और `retry_allowed: true` जैसी self-contradictory combination रख सकता है। Schema validation आमतौर पर हर business invariant को express नहीं करती। Contract test को assert करना चाहिए कि stray boolean के बावजूद successful job कोई retry plan न बनाए।

## Convenient mock नहीं, पूरा action route test करें

Parser के unit tests जरूरी हैं, लेकिन वे साबित नहीं करते कि deployed agent अपने वास्तविक credential और execution route से intended request भेजेगा। Serialization libraries, wrappers, tool adapters और retry middleware direct function call में न दिखने वाले behavior बदल सकते हैं।

CI में local contract server चलाएं, जो requests record करे और versioned fixtures लौटाए। Agent के tool configuration को उस server पर point करें। Test को realistic instruction चलानी चाहिए, proposed plan या action record का इंतजार करना चाहिए और recorded sequence assert करनी चाहिए: method, path, query, सुरक्षित रूप से inspect किए जा सकने वाले headers, body और action count।

इस environment में live secrets न रखें। ऐसा test credential इस्तेमाल करें जिसके पास कोई authority न हो और verify करें कि agent को वह prompt, tool result, exception text या trace में कभी न मिले। Request headers को लापरवाही से log करने वाला test उसी credential exposure को दोहरा सकता है जिसे रोकना उसका उद्देश्य था।

Sallyport के जरिए external HTTP या SSH calls करने वाले agents में action route credentials को agent से बाहर रखते हुए inspectable result दे सकता है। यह boundary response की गलत interpretation ठीक नहीं करती, इसलिए external action की अनुमति देने से पहले schema और decision contracts चलाएं।

केवल golden responses नहीं, failure fixtures भी शामिल करें। Changeover के दौरान providers द्वारा लौटाई जाने वाली exact conditions दें: missing selection field, नया enum member, continuation cursor वाला empty result, content type change और error object वाला 200 response। पुराने APIs में 200 error body खास तौर पर आम है। अगर parser मानता है कि हर 200 response में success shape है, तो वह empty plan बना सकता है या पहले ही सफल request को retry कर सकता है।

Retries test करते समय idempotency behavior assert करें। Contract server को ऐसा first response दें जो request record करने के बाद timeout हो, फिर retry के लिए दूसरा response दें। Test साबित करे कि जहां API support करती है वहां client idempotency token भेजता है, या जहां first action पूरा हुआ या नहीं यह पता नहीं चल सकता वहां रुककर confirmation मांगता है। Read को retry करना आमतौर पर harmless है। Transfer, email, deployment या delete request को retry करना नहीं।

## Release gates को semantic breakage रोकनी चाहिए

API producers और agent consumers दोनों के लिए change path में schema diffs, provider contract tests और agent decision tests चलाएं। Deployment के बाद चलने वाली release process tests को incident documentation में बदल देती है।

Provider change के लिए review record में चार concrete questions का उत्तर अनिवार्य करें: कौन से consumer assumptions बदलेंगे, पुराना behavior क्या था, दोनों behaviors कितने समय तक supported रहेंगे और नया behavior दिखाने वाला fixture कौन सा है। अधूरे logs के साथ production regression पर बहस करने से यह कम paperwork है।

Agent change के लिए merge से पहले मौजूदा provider fixture suite चलाएं। अगर agent अब नया field इस्तेमाल करता है, तो उसके absence और happy path से बाहर के values के fixtures जोड़ें। Prompt wording में बदलाव भी tool arguments बदल सकता है, इसलिए पूरे agent द्वारा emit की गई tool call test करें, यह मानकर न चलें कि planner वही parameters चुनता रहेगा।

Versioned fixtures review को practical बनाते हैं। Expected request और response pair के साथ `projects-list-v1` जैसा identifier रखें। Provider जानबूझकर `projects-list-v2` लाए, तो migration policy समाप्त होने तक पुराना fixture रखें। पुराने JSON को overwrite करके tests को current न कहें। इससे dropped compatibility का proof खो जाता है।

Useful gate operational language में failures report करता है। «Expected field owner missing, archive planning stopped» reviewer को बताता है कि क्या हुआ। «ValidationError at path items.0» से बेहतर कुछ नहीं होने से यह अच्छा है, लेकिन release के दौरान reviewer को risk खुद reconstruct करना पड़ता है।

## Misleading plan को approval screen ठीक नहीं कर सकती

External actions के लिए human approval अभी भी अच्छा control है, लेकिन बदले हुए contract से agent ने गलत plan बना लिया हो तो approval देर से आता है। «Archive 847 inactive projects» देखकर व्यक्ति उसे reject कर सकता है। «Archive project p17» देखकर वह नहीं जान सकता कि p17 absent owner field, बढ़े हुए default या `cancelled` को `failed` समझने वाले parser से आया है।

Approval records में वे decision inputs शामिल करें जिनकी human review जरूरी है: target identifiers, count, requested mode और वे response fields जिनसे target eligible बना। Record compact रखें। Approver को raw JSON देना parsing का काम code से हटाकर थके हुए व्यक्ति पर डालना है।

इसके साथ ऐसा trace सुरक्षित रखें जिससे engineer action reconstruct कर सके। Provider response या उसका protected digest, parser version, contract fixture version, generated request और resulting response capture करें। Tamper-evident audit trail बाद में मदद करता है, लेकिन उसे केवल HTTP call होने का record नहीं, decision boundary दिखानी चाहिए।

अगली बार API team कहे कि response change cosmetic है, तो उनसे agent contract suite चलाने को कहें। अगर suite fail होती है, तो change के साथ behavior जुड़ा हुआ है। एक विनम्र 200 response unsafe action बन जाए, उससे पहले उसे ठीक करें।
