# AI Agents के लिए सुरक्षित APIs: Trace की जा सकने वाली Writes

AI agent अपने आप किसी API को खतरनाक नहीं बनाता। API तब खतरनाक बनती है जब उसमें बहुत व्यापक verbs, अस्पष्ट परिणाम और ऐसे errors हों जिनकी वजह से caller को अनुमान लगाना पड़े। इंसानी operators context, सावधानी और chat में एक जल्दी से भेजे गए message से इसकी भरपाई करते हैं। Agent retries और एक और tool call से इसकी भरपाई करता है। यही अंतर एक सामान्य timeout को दो refunds, दो deployments या गलती से delete हुए record में बदल देता है।

AI agents के लिए सुरक्षित APIs अनुमत action को छोटा रखती हैं, बार-बार की गई writes को harmless बनाती हैं और ऐसा trail छोड़ती हैं जिसे कोई व्यक्ति बाद में follow कर सके। यह API design का काम है, prompt का नहीं। Prompt agent को सावधान रहने के लिए कह सकता है, लेकिन endpoint को अपने contract से बाहर की action फिर भी reject करनी होगी।

मैंने teams को एक ढीले administrative endpoint के सामने approval screens लगाकर उसे control कहते देखा है। यह पर्याप्त नहीं है। अगर approved call का अर्थ है «इस account में कुछ भी बदल दो», तो approval के दौरान इंसान से समय के दबाव में छिपे हुए परिणामों के पूरे bundle की जांच करने को कहा जा रहा है। Precision पहले API में रखें। तब approval के सामने approve करने के लिए कुछ स्पष्ट होगा।

## Broad verbs agents को अनुमान लगाने पर मजबूर करते हैं

Agent को ऐसी operation call करनी चाहिए जिसका नाम, inputs और side effects एक वाक्य में समझ आ जाएं। Broad endpoints उसे loose fields, पुराने examples या केवल «bad request» कहने वाले error से business rules का अनुमान लगाने पर मजबूर करते हैं। Unsafe improvisation यहीं से शुरू होती है।

मान लीजिए कोई endpoint `POST /admin/execute` है और उसके payload में `action` तथा arbitrary JSON है। इंसान द्वारा लिखा client आज केवल पांच actions इस्तेमाल कर सकता है, लेकिन endpoint access पाने वाले हर caller के सामने वर्तमान और भविष्य की सभी actions रखता है। Server उपयोगी permission boundary नहीं बता सकता और approver payload को source code की तरह पढ़े बिना यह नहीं समझ सकता कि agent क्या करेगा।

इसके बजाय ऐसी operations बनाएं जो state transition का नाम बताएं:

- `POST /projects/{project_id}/deployments` किसी निर्धारित revision से एक deployment बनाता है।
- `POST /invoices/{invoice_id}/refunds` स्पष्ट amount और reason के साथ refund बनाता है।
- `POST /users/{user_id}/access-revocations` एक नामित user का access हटाता है।
- `POST /exports` घोषित data category के साथ एक परिभाषित export शुरू करता है।

इन operations में भी risk हो सकता है। बात यह है कि हर operation server को rules लागू करने की स्पष्ट जगह देती है: valid transitions, amount limits, target ownership, required approvals और सही जगह पर reason field।

Generic CRUD interface को usable agent interface न समझें। `PATCH /customers/{id}` caller को किसी भी writable field को बदलने का निमंत्रण देता है। अगर `billing_email` बदलना सामान्य काम है, लेकिन `tax_status` बदलने से compliance process शुरू होता है, तो दोनों को एक ही casual patch के पीछे नहीं होना चाहिए। Consequential transition के लिए focused operation बनाएं और उसका input model उसी decision के अनुसार रखें।

Narrow operation recovery को भी बेहतर बनाती है। जब agent कहता है «deployment request timed out», तो operator एक deployment creation खोज सकता है। जब वह कहता है «admin command timed out», तो operator को पहले यह पता लगाना पड़ता है कि agent ने कौन सा command बनाया था।

### Preconditions को request में रखें

Writes को वह condition बतानी चाहिए जिसके तहत वे उचित हैं। Expense approve करने वाली request में expected review state हो सकती है। Document update करने वाली request में वह version शामिल हो सकती है जिसे client ने पढ़ा था। अगर state बदल चुकी है, तो server को write को किसी दूसरी वास्तविकता पर चुपचाप लागू करने के बजाय उसे reject करना चाहिए।

HTTP पहले से उपयोगी mechanics देता है। RFC 9110 `If-Match` जैसे headers के जरिए conditional requests परिभाषित करता है। Server पुराने entity tag को `412 Precondition Failed` के साथ reject कर सकता है। API के लिए सुविधाजनक हो तो `expected_version` field भी expose की जा सकती है। चुनाव से ज्यादा महत्वपूर्ण discipline है: client को वह version या state बतानी होगी जिसे वह बदलना चाहता है।

हर conflict से बचने के लिए `force: true` जैसे client field को escape hatch के रूप में स्वीकार न करें। यह field अक्सर उस safety check को bulldoze करने का तरीका बन जाती है जिसे आपने खुद जोड़ा था। Override को अलग operation, अलग authorization level और visible audit record के लिए रखें।

## Write की identity HTTP attempt से अलग होनी चाहिए

हर externally visible write में client द्वारा दिया गया idempotency identifier होना चाहिए। Server इसका इस्तेमाल यह पहचानने के लिए करता है कि delivery की कई attempts एक ही intended action हैं।

Request ID और idempotency identifier अलग-अलग failures को हल करते हैं। Gateway या server अक्सर हर HTTP attempt के लिए request ID बनाता है। अगर server write commit करने के बाद response caller तक पहुंचने से पहले network connection खो देता है, तो retry को नया request ID मिलता है। Idempotency identifier वही रहना चाहिए, क्योंकि intended write बदली नहीं है।

आम failure sequence कुछ ऐसी होती है:

1. Agent payout बनाने के लिए request भेजता है।
2. आपका server payout store करता है और upstream provider को call करता है।
3. Agent को success response मिलने से पहले connection टूट जाता है।
4. Agent unknown outcome देखता है और retry करता है।
5. आपका server दूसरा payout बना देता है, क्योंकि उसे नया HTTP request दिखाई देता है।

Defect retry policy ने नहीं पैदा किया। API ने delivery को intent मानकर यह गलती की।

ऐसा header या request field इस्तेमाल करें जिसे client अपनी पहली attempt से पहले बनाए और terminal answer मिलने तक सुरक्षित रखे। HTTP header names में आम तौर पर `Idempotency-Key` इस्तेमाल होता है, हालांकि identifier को secret होना जरूरी नहीं। Random UUID अच्छा काम करता है। इसे केवल timestamp से न बनाएं और ऐसा identifier भी न इस्तेमाल करें जो असंबंधित writes के बीच collide कर सकता हो।

### Request fingerprint और result store करें

Server को idempotency identifier को केवल status flag से अधिक चीजों के साथ bind करना चाहिए। Caller identity, target route, semantically relevant request body का canonical fingerprint और replay के लिए जरूरी पूरा result store करें। जब वही caller उसी identifier और उसी fingerprint के साथ retry करे, तो original response लौटाएं। Body अलग हो, तो conflict के साथ request reject करें।

IETF draft «The Idempotency-Key HTTP Header Field» इस header को clients के लिए non-idempotent HTTP methods को fault tolerant बनाने के तरीके के रूप में बताता है। Uniqueness की उसकी चेतावनी महत्वपूर्ण है: client को किसी अलग request के लिए वही value reuse नहीं करनी चाहिए। Implementation में मैं एक कदम और आगे जाऊंगा। इस चेतावनी को server-side enforce करें, क्योंकि agents retry और restart करते हैं और कभी-कभी ऐसी state reuse कर लेते हैं जिसे human client छोड़ चुका होता।

एक compact contract ऐसा दिख सकता है:

```http
POST /v1/projects/prj_48/deployments
Idempotency-Key: 8c8d77c1-4ef9-4fae-b0ba-5480f686ce4c
Content-Type: application/json

{
  "revision": "a1b2c3d4",
  "environment": "staging",
  "expected_project_version": 17
}
```

पहली accepted call पर resource और दोनों identifiers लौटाएं:

```json
{
  "request_id": "req_01J8X7QK3JZ6",
  "deployment": {
    "id": "dep_01J8X7R5G2",
    "state": "queued",
    "revision": "a1b2c3d4",
    "environment": "staging"
  }
}
```

अगर timeout के बाद agent बिल्कुल वही request दोहराता है, तो दूसरा deployment बनाने के बजाय वही `dep_01J8X7R5G2` लौटाएं। अगर identifier वही रखते हुए वह `environment` को `production` में बदल देता है, तो ऐसा conflict लौटाएं जिससे सुधार स्पष्ट हो:

```json
{
  "error": {
    "code": "idempotency_payload_mismatch",
    "message": "This idempotency identifier belongs to a deployment request with different parameters.",
    "request_id": "req_01J8X84S9P2V"
  }
}
```

Idempotency records को कम से कम उतने समय तक रखें जितने समय में वास्तविक client retries और job recovery हो सकती है। बहुत कम retention period production में intermittent दिखने वाला delayed duplicate बना देता है। Storage pressure के कारण expiry करनी पड़े, तो window को साफ शब्दों में document करें और consumers से ऐसा retry behavior चुनने को कहें जो उसका सम्मान करे।

## Retry तभी सार्थक है जब outcome पर्याप्त रूप से ज्ञात हो

Agent को transport failures और चुने हुए transient responses पर retry करनी चाहिए, लेकिन uncertainty से बचने के लिए उसे कभी नई action नहीं गढ़नी चाहिए। आपके response classes को यह चुनाव संभव बनाना होगा।

RFC 9110 `429 Too Many Requests` को परिभाषित करता है और `Retry-After` की अनुमति देता है। अगर आप इसे भेजते हैं, तो उस mechanism का सम्मान करें। Caller बताए गए समय तक रुक सकता है, अपना idempotency identifier सुरक्षित रख सकता है और वही request फिर भेज सकता है। Temporary server failure के लिए request ID वाला 5xx response दें और स्पष्ट करें कि server ने operation स्वीकार की या नहीं। Validation failure या authorization refusal के लिए अस्पष्ट 500 न भेजें। इससे clients को गलत retry behavior सीखने को मिलता है।

Asynchronous writes में acceptance और completion दो अलग facts हैं। `202 Accepted` response को work और उसकी state बताने वाली operation resource लौटानी चाहिए। Timeout के बाद agent side effect दोबारा submit करने के बजाय उस resource को query कर सकता है।

```json
{
  "request_id": "req_01J8X9FW7GH2",
  "operation": {
    "id": "op_01J8X9FTVX",
    "state": "running",
    "status_url": "/v1/operations/op_01J8X9FTVX"
  }
}
```

Status resource में केवल `running` और `failed` पर्याप्त नहीं हैं। Terminal state, सफलता पर result reference और worker job पूरा न कर पाए तो public failure code शामिल करें। उदाहरण के लिए, health checks में असफल deployment को API transport failure जैसा नहीं दिखना चाहिए। Agent को deployment failure report या repair करना चाहिए; connection failure पर ही retry करना चाहिए, वह भी तब जब server ने request कभी स्वीकार न की हो।

Email भेजने, पैसे charge करने, credentials rotate करने या बाहरी system को call करने वाली actions पर automatic retries से बचें, जब तक आपका server effect तक deduplication का पूरा नियंत्रण न रखता हो। Database में idempotency होने से दो emails भेजने से बचाव नहीं होता, अगर worker email provider के message स्वीकार करने के बाद और completion record करने से पहले crash हो जाए। Provider support करे तो stable provider-side deduplication reference वाले outbox record का इस्तेमाल करें। बाहरी system deduplicate न कर सके, तो operation को observable बनाएं और unknown outcome के बाद human decision जरूरी करें।

## Errors caller को request सुधारने का तरीका बताएं

Useful error messages server की शर्मिंदगी नहीं, failed contract का विवरण देते हैं। Agent precise error के साथ काम कर सकता है। Ten fields वाली request के बाद HTML error page, stack trace या «invalid input» के साथ वह सुरक्षित ढंग से काम नहीं कर सकता।

हर expected failure के लिए consistent JSON envelope लौटाएं। Programs के लिए stable `code`, logs और humans के लिए concise `message`, request ID और सुरक्षित रूप से दिखाए जा सकने वाले fields की details दें। RFC 9457, «Problem Details for HTTP APIs», `type`, `title`, `status`, `detail` और `instance` जैसी fields के जरिए एक standard shape देता है। हर field अपनाना जरूरी नहीं, लेकिन उसका मुख्य lesson अपनाएं: errors API contract का हिस्सा हैं, incidental prose नहीं।

यह response agent को ठीक-ठीक बताता है कि क्या बदलना है:

```json
{
  "error": {
    "code": "invalid_state_transition",
    "message": "A refund can be created only for a paid invoice.",
    "request_id": "req_01J8XAS2D8M4",
    "details": {
      "invoice_id": "inv_204",
      "current_state": "draft",
      "allowed_states": ["paid", "partially_paid"]
    }
  }
}
```

यह response अनुमान लगाने पर मजबूर करता है:

```json
{
  "error": "Request failed"
}
```

दूसरा response agent को documentation, source code या exploratory calls की ओर भेजता है। Write API के खिलाफ exploratory calls ही छोटे defect को noisy incident में बदलती हैं।

Errors में secrets न रखें। Authorization headers, access tokens, signed URLs, raw database queries या upstream service का ऐसा response वापस न करें जिसमें किसी दूसरे customer का data हो सकता है। हर caught exception को wrap करके उसका message caller को लौटाना एक आम खराब pattern है। इससे एक दिन debugging आसान होती है, लेकिन वर्षों के लिए disclosure channel बन जाता है।

Invalid input को insufficient authority से अलग रखें। `422 Unprocessable Content` ऐसे well-formed payload का वर्णन कर सकता है जो business rule का उल्लंघन करता है। `403 Forbidden` को कहना चाहिए कि requested operation के लिए permission या approval चाहिए, लेकिन उसे ऐसी resources के बारे में जानकारी नहीं देनी चाहिए जिन्हें caller देख नहीं सकता। Resource का अस्तित्व जानबूझकर छिपाना हो तो `404 Not Found` उचित हो सकता है। Semantics चुनें, document करें और लगातार लागू करें।

अच्छा error यह भी बताता है कि retry करना बेकार है। `invalid_state_transition`, `idempotency_payload_mismatch` और `approval_required` blind retries रोकने चाहिए। `rate_limited` के साथ retry delay और `upstream_temporarily_unavailable` controlled retry का संकेत दे सकते हैं। यह अंतर clever agent prompt से ज्यादा damage बचाता है।

## Request identifiers disputed action को investigation में बदल देते हैं

हर inbound request को request ID दें, उसे response body या header में लौटाएं और हर internal call, queue message, worker job तथा outbound provider call के साथ आगे भेजें। जब agent कहता है कि उसे response नहीं मिला, तो आपको दो अलग सवालों का जवाब देना होता है: क्या आपकी API ने action स्वीकार की और उसके बाद हर component ने क्या किया?

अगर caller request ID नहीं देता, तो trust boundary पर request ID बनाएं। Caller को अपने bookkeeping के लिए correlation ID देने दें, लेकिन untrusted caller को server-issued identifier overwrite न करने दें। जरूरत हो तो दोनों रखें। Server identifier आपके logs को anchor करता है; caller identifier agent decisions की एक sequence को जोड़ता है।

ऐसे prose lines बनाने के बजाय structured events log करें जिन्हें operators बाद में regular expressions से parse करें। कम से कम request ID, authenticated principal, operation name, target resource, मौजूद हो तो idempotency identifier, authorization decision, result status और बनाई गई resources के references record करें। Request fields को last-minute string filter से नहीं, schema के आधार पर redact करें। `token` नाम वाले field को redact करना आसान है। Arbitrary text में छिपे credential को नहीं।

Trace को ordering सुरक्षित रखनी चाहिए, लेकिन उससे ज्यादा साबित होने का दावा नहीं करना चाहिए। Request ID दिखा सकती है कि आपकी API ने job स्वीकार की और worker ने provider call submit की। यह साबित नहीं कर सकती कि किसी व्यक्ति ने action का इरादा किया था, जब तक आपका system उस decision को अलग से record न करे। अंतर स्पष्ट रखें:

- Correlation record एक request से जुड़े events को जोड़ता है।
- Audit record बताता है कि किसने या किस चीज ने action authorize की और system ने क्या किया।
- Idempotency record एक duplicate logical write रोकता है।

Teams अक्सर इन्हें एक database row में मिला देती हैं। फिर वही row retries, debugging, compliance review और user-facing history, सभी का काम करने लगती है और कोई भी काम साफ ढंग से नहीं होता। Related references एक साथ store कर सकते हैं, लेकिन data model में उनके अलग-अलग अर्थ सुरक्षित रखें।

Higher-risk actions के लिए normalized request, authorization context, policy या approval outcome और result digest को append-only audit stream में record करें। उस stream को normal application account से सुरक्षित रखें। वरना compromised service उस history को बदल सकती है जो उसके compromise को उजागर करती।

Sallyport agent actions के लिए एक उपयोगी तरीका अपनाता है: यह agent sessions और individual calls को एक write-blind encrypted, hash-chained audit log में record करता है और `sp audit verify` vault key के बिना offline chain की जांच करता है। फिर भी आपकी API को अपने records चाहिए, क्योंकि gateway यह दिखा सकता है कि उसने call dispatch की, जबकि केवल आपकी service यह दिखा सकती है कि उसने कौन सा state transition commit किया।

## Authentication scope असुरक्षित operation को सुरक्षित नहीं करता

Short-lived credentials और narrow scopes blast radius घटाते हैं, लेकिन broad endpoint को सुरक्षित नहीं बनाते। एक project तक सीमित token भी उस project के हर deployment को destroy, सभी permitted data को export या उपलब्ध हर administrative action को trigger कर सकता है।

Authorization को operation और target से जोड़ें। Deployment बनाने की अनुमति रखने वाले caller को उसे production में promote करने की permission अपने आप नहीं मिलनी चाहिए। User का access revoke करने वाले caller को उस user का billing profile बदलने की permission इसलिए नहीं मिलनी चाहिए कि दोनों `/users/{id}` के नीचे आते हैं।

जब संभव हो, credential material को agent से दूर रखें। Bearer token पाने वाला agent उसे transcript, debug file, shell history या external service call में copy कर सकता है। इसके बजाय credential use को local action gateway या server-side broker के पीछे रखें, जो approved operation के लिए credential चुनता है। Agent intent और parameters submit करता है; trusted component outbound call करते समय ही secret inject करता है।

इस design से parameters validate करने की जरूरत खत्म नहीं होती। अगर agent `url: https://anything.example` कह सकता है, तो credential-injecting HTTP helper secret exfiltration tool बन सकता है। Credentials को named upstreams और methods से bind करें। Redirects से पहले ही नहीं, उनके बाद भी hosts validate करें। SSH के लिए credential को known hosts और जहां संभव हो constrained command interface से bind करें, arbitrary remote shell access न दें।

Human approval की जगह है, लेकिन उसे visible target और consequence वाली छोटी action तक सीमित रखें। Per-session approval यह जवाब देता है, «क्या यह agent process बिल्कुल काम कर सकता है?» Per-call approval पूछता है, «क्या यह अभी यह खास sensitive action कर सकता है?» इनमें से कोई भी ऐसा endpoint सुरक्षित नहीं बना सकता जिसके payload का अर्थ कुछ भी हो।

## Concurrency को स्पष्ट loser चाहिए

Idempotency एक intention की duplicate delivery रोकती है। यह दो अलग intentions की race को हल नहीं करती। अगर दो agents invoice को `paid` state में पढ़ते हैं और अलग idempotency identifiers के साथ full refund submit करते हैं, तो आपके server को तय करना होगा कि कौन सी request जीतेगी।

जहां storage system support करे, transactional state transition इस्तेमाल करें। Update में expected state शामिल होनी चाहिए और किसी दूसरे writer के पहले state बदलने पर server को conflict report करना चाहिए। Version field, entity tag या conditional update API को stale intent reject करने का तरीका देता है, बजाय facts बदलने के बाद उसे लागू करने के।

उदाहरण के लिए, refund को client द्वारा दिए गए amount पर भरोसा करने वाले blind command के बजाय remaining refundable balance पर operation की तरह model करें। एक transaction में current paid amount जांचें, prior refunds घटाएं, requested amount validate करें, नया refund reserve करें और refund record बनाएं। इसके बाद अलग asynchronous worker payment provider को call कर सकता है। Worker retry करे, तो नया record बनाने के बजाय उसी refund record से आगे बढ़े।

Agents को केवल «पहले check करो, फिर act करो» बताना concurrency control नहीं है। Preflight `GET` agent को useful request बनाने में मदद करता है, लेकिन read और write के बीच कोई दूसरा caller state बदल सकता है। Correctness की जिम्मेदारी write endpoint की है, क्योंकि commit के समय वही actual state देखता है।

Cancellation को भी इसी सावधानी से design करें। `DELETE /operations/{id}` को यह promise नहीं करनी चाहिए कि external action कभी हुई ही नहीं। उसे actual cancellation state लौटानी चाहिए: cancellation requested, dispatch से पहले cancelled, cancellation से पहले completed या dispatch के बाद cancellation impossible। Agents और humans, दोनों को ऐसी भाषा चाहिए जो आपके system और external provider के बीच की सीमा को सही दिखाए।

## Production में agents के मिलने से पहले unknown outcomes की जांच करें

केवल 200 response जांचने वाली test suite action APIs के सबसे कठिन हिस्से को नजरअंदाज करना सिखाती है। Failure cases को contract tests में रखें और उन्हें केवल mocked handler पर नहीं, real service boundary के खिलाफ चलाएं।

हर write operation के लिए वह sequence test करें जिसमें server effect commit करता है और client response खो देता है। उसी idempotency identifier को फिर submit करें और assert करें कि server original resource लौटाता है। फिर उसी identifier को बदली हुई body के साथ submit करें और assert करें कि server कोई दूसरी resource बनाए बिना conflict लौटाता है।

अलग idempotency identifiers वाली concurrent requests को उसी state transition के खिलाफ test करें। Assert करें कि एक सफल होती है और दूसरी को specific stale-state या business-rule error मिलता है। अगर test database में दोनों succeed कर जाती हैं क्योंकि हर test अकेले चलती है, तो आपने महत्वपूर्ण property test नहीं की है।

Error contract को data की तरह exercise करें। Status codes, stable error codes, field names और request ID की मौजूदगी assert करें। केवल English message का snapshot न लें। Wording समय के साथ सुधरेगी; clients को prose के बजाय `code` पर branch करना चाहिए।

अंत में operator drill चलाएं। एक completed action, एक rejected action, successful server-side commit वाला timeout और एक asynchronous failure चुनें। Engineer को केवल request IDs दें और उससे पूछें कि क्या हुआ, यह reconstruct करे। अगर उसे असंबंधित logs में grep करना पड़े, agent transcript देखना पड़े और अनुमान लगाना पड़े कि कौन सी retry से कौन सा record बना, तो unattended writes की अनुमति देने से पहले instrumentation ठीक करें।

सबसे पहले repair करने वाली action आम तौर पर सबसे broad write endpoint होती है। उसे named transitions में बांटें, idempotency identifier जरूरी करें और response में बनने वाली resource की पहचान दें। यह contract बन जाने के बाद agents तेजी से काम कर सकते हैं, बिना हर network hiccup को कुछ अलग करने की permission समझे।
