# क्या MCP की क्षमता में डाउनग्रेड एजेंट नियंत्रणों को कमजोर करते हैं?

प्रोटोकॉल compatibility भी attack surface का हिस्सा है। जब कोई MCP क्लाइंट पुराने वर्जन पर जाता है या कोई capability छोड़ देता है, तो उसे केवल सुविधाएं खोनी चाहिए, वे controls नहीं जो किसी एजेंट को किसी और के अधिकार से काम करने से रोकते हैं।

मैंने यह failure एक मामूली compatibility patch की तरह आते देखा है: क्लाइंट notification feature का विज्ञापन नहीं करता, इसलिए code पुराने branch पर चला जाता है; वह branch per-session approval आने से पहले लिखी गई थी; कोई action इसलिए आगे बढ़ जाता है क्योंकि किसी ने उस branch को authorization path माना ही नहीं। Code happy-path tests पास करता रहता है। फिर भी वह गलत है।

MCP capability downgrades के लिए security tests ज़रूरी हैं, क्योंकि initialization metadata gateway के भीतर उस रास्ते को बदल देता है जिसे agent की पहली sensitive request से पहले अपनाया जाता है। उस रास्ते को adversary की तरह जांचें: पुराना protocol version घोषित करें, fields छोड़ें, खाली capability object भेजें, process restart करें, vault lock करें, session revoke करें और बाद में audit journal में दर्ज जानकारी देखें। अगर जवाब «deny या ask» से बदलकर «run» हो जाता है, तो compatibility credential bypass बन चुकी है।

## Downgrade route बदलता है, authority नहीं

Compatibility path message shapes, उपलब्ध notifications, progress handling या क्लाइंट को मिलने वाले context की मात्रा बदल सकता है। उसे यह नहीं बदलना चाहिए कि credential इस्तेमाल करने की अनुमति किसे है या call के लिए किसी व्यक्ति की स्वीकृति ज़रूरी है या नहीं।

यह अंतर तब तक साफ लगता है जब तक code `protocolVersion` या `capabilities` के आधार पर branches बनाना शुरू नहीं करता। Developer अक्सर unsupported server message भेजने से बचने के लिए branch लिखता है। बाद में कोई व्यक्ति उसी branch में session setup, approval delivery या audit initialization डाल देता है, क्योंकि वे पास-पास होते हैं। इस तरह downgrade अनजाने में security boundary बदल देता है।

MCP specification initialization को ऐसे exchange के रूप में परिभाषित करती है जिसमें क्लाइंट protocol version और capabilities घोषित करता है और server अपना version तथा capabilities लौटाता है। इन fields को untrusted peer के claims मानें। वे interoperability में मदद कर सकते हैं। वे power नहीं दे सकते।

Sallyport इस separation को व्यावहारिक बनाता है: उसका vault gate, session authorization और per-call key setting MCP conversation के नीचे काम करते हैं। इसलिए कोई agent खुद को पुराने क्लाइंट के रूप में बताकर credential नहीं पा सकता। यह सही संरचना है, लेकिन protocol boundary पर इसके लिए regression tests अब भी ज़रूरी हैं।

Matrix लिखने से पहले security invariant लिखें:

> हर स्वीकार किए गए MCP initialization variant के लिए, vault locked होने पर protected action को रोकना होगा; नए क्लाइंट प्रोसेस को session approval चाहिए; per-call review के लिए चिह्नित credential के हर इस्तेमाल पर review चाहिए; और action सफल हो, विफल हो या अस्वीकार हो, audit event बनना चाहिए।

यह वाक्य reviewers को «legacy clients work» से कहीं स्पष्ट आधार देता है। Legacy client तभी काम कर सकता है जब वह इन outcomes को बनाए रखे।

## Initialization input के लिए hostile test cases जरूरी हैं

अच्छा क्लाइंट एक साफ initialization request भेजता है, अपनी supported capabilities की सूची देता है और response मिलने के बाद आगे बढ़ता है। Security tests यहीं से शुरू करें, फिर एक-एक करके हर assumption हटाएं।

एक छोटा client fixture इस्तेमाल करें जो high-level MCP library के बजाय raw JSON-RPC messages भेज सके। Library tests उपयोगी हैं, लेकिन वे उन wire conditions को छिपा देते हैं जिनसे downgrade bugs पैदा होते हैं।

Baseline request कुछ ऐसी दिख सकती है:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "CURRENT_TEST_VERSION",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": { "name": "compat-fixture", "version": "1.0" }
  }
}
```

इसके बाद fixture को ऐसे variants बनाने चाहिए जिनमें केवल एक महत्वपूर्ण अंतर हो:

- सबसे पुराना supported protocol version
- `{}` को capabilities के रूप में रखने वाला current version
- `capabilities` के बिना current version, अगर आपका parser इसे स्वीकार करता है
- ऐसा capability object जिसमें ज्ञात optional entries न हों
- ऐसा unsupported older version जिसे server को अस्वीकार करना चाहिए

पहले test में सभी omissions को एक साथ न मिलाएं। जब कोई combined malformed request विफल होती है, तो आप बहुत कम सीखते हैं। एक-चर वाले cases बताते हैं कि किस default या branch ने परिणाम बदला।

हर accepted case में approval से पहले protected action का एक जैसा outcome assert करें। Exact transport response implementation के अनुसार अलग हो सकता है, लेकिन उसका रूप स्पष्ट होना चाहिए:

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "Session authorization required"
  }
}
```

केवल text assert न करें। Stable error category या internal result code, किसी भी outbound HTTP request या SSH invocation की अनुपस्थिति और denied attempt का audit record, इन सभी को assert करें। Friendly error के पीछे वास्तविक side effect हो, तो test विफल है।

मुश्किल मामला वह initialization request है जिसके fields parse तो हो जाते हैं, लेकिन आपस में मेल नहीं खाते। हो सकता है क्लाइंट कहे कि वह ऐसा version बोलता है जिसे आपका server पहचानता है, लेकिन वह ऐसी कोई ability घोषित न करे जिसका सामान्य क्लाइंट उस version पर इस्तेमाल करता। आपके gateway को क्लाइंट को भाषण देने की जरूरत नहीं है। उसे सुरक्षित रास्ता चुनना है: limited session स्वीकार करे और सभी local controls बनाए रखे, या initialization अस्वीकार करे। उसे केवल इसलिए unprotected route नहीं चुनना चाहिए कि सामान्य approval presentation उपलब्ध नहीं है।

## पुराने wire formats पुराना security model नहीं बना सकते

Teams अक्सर कहते हैं कि वे पुराने protocol version को support करते हैं, जबकि उनका मतलब होता है कि वे उसके messages parse कर सकते हैं। यह काम का केवल आधा हिस्सा है। आपको यह भी तय करना होगा कि पुराने wire format में किसी current security behavior के लिए field न होने पर वह behavior कैसे बना रहेगा।

जवाब आम तौर पर पूरा behavior होना चाहिए। Session authorization local state है। Per-call review local state है। Audit capture local state है। इनमें से किसी के लिए पुराने request में equivalent field होना ज़रूरी नहीं।

यहीं capability negotiation और authorization आपस में गड्डमड्ड हो जाते हैं। हो सकता है किसी क्लाइंट में structured status update पाने की सुविधा न हो। इससे यह पता चलता है कि approval state कैसे बतानी है, यह नहीं कि approval छोड़ देनी है। अगर आपका product उस invocation के लिए user को review नहीं दिखा सकता, तो call को स्पष्ट error के साथ रोक दें। क्लाइंट से यह वादा करवाना कि उसने prompt दिखाया था, local decision का विकल्प नहीं है।

«अगर क्लाइंट per-call prompts संभाल नहीं सकता, तो पूरे session को approve कर दें» वाला लोकप्रिय fallback न अपनाएं। यह लोकप्रिय इसलिए है कि friction हटती है और demo पास हो जाता है। यह गलत है, क्योंकि credential owner की हर इस्तेमाल वाली decision को उनकी सहमति के बिना one-time decision में बदल देता है। Credential की setting को प्राथमिकता मिलनी चाहिए।

Version adapter को सीमित रखें। उसे request और response syntax का अनुवाद करना चाहिए, peer द्वारा न समझे जाने वाले messages को filter करना चाहिए और compatible error forms को map करना चाहिए। उसे यह तय नहीं करना चाहिए कि action execute होगा या नहीं। Authorization को एक ऐसे path में रखें जिसे हर adapter call करे।

Code review में एक उपयोगी सवाल है: क्या कोई compatibility branch नवीनतम क्लाइंट जैसी authorization function से गुजरे बिना execution handle, injected authorization header या SSH result लौटा सकती है? अगर जवाब हां है, तो उस branch के लिए अभी test और जल्द redesign चाहिए।

## Session approval उसी process के साथ चलनी चाहिए जिसने request की

Per-session approval का गलत test लिखना आसान है। अगर test client एक ही process को जीवित रखता है, उसे approve करता है और दस calls चलाता है, तो आप केवल यह साबित करते हैं कि approved session सामान्य स्थिति में चलता रहता है। आपने यह साबित नहीं किया कि अगले agent process को अपना decision चाहिए।

दो independently launched fixture processes इस्तेमाल करें। उन्हें वही displayed client name, वही protocol version और वही capability declaration दें। Process A initialize होकर protected action मांगे। उसके session को approve करें। फिर Process B initialize होकर वही action मांगे।

Process B को नया approval requirement मिलना चाहिए। उसे A की authorization विरासत में नहीं मिलनी चाहिए, भले ही वह client label, working directory, transport endpoint या cached connection detail साझा करता हो। अगर आपका implementation approval card में code-signing authority दिखाता है, तो assert करें कि card उस process authority की पहचान करता है जिसने वास्तव में B को launch किया। Test में card के सजावटी हिस्सों को assert न करें, बल्कि उस identity signal को assert करें जिसका इस्तेमाल कोई व्यक्ति निर्णय लेने के लिए करता है।

फिर production agents से जुड़े lifecycle edges जांचें:

1. Process A को approve करें और उसे सामान्य रूप से exit होने दें। समान metadata के साथ Process B शुरू करें। B को approval चाहिए।
2. A को approve करें, फिर graceful shutdown के बिना उसे terminate करें। B शुरू करें। B को approval चाहिए।
3. A को approve करें, फिर उसके जीवित रहते उसे session journal से revoke करें और A से एक और call करें। Execution से पहले वह call रुकनी चाहिए।
4. जब A की पहली action external response का इंतजार कर रही हो, तब A को revoke करें। Revocation के बाद pending action को approved second action नहीं बनाना चाहिए।

तीसरा और चौथा मामला एक सामान्य गलती सामने लाते हैं: system केवल connection खोलते समय या action object बनाते समय approval जांचता है। कोई process चल रहा हो, तब भी session बदल सकता है। Gateway के outbound action पर commit करने के बिंदु पर authority जांचें।

Caller द्वारा दिए गए session ID को approval key न बनाएं। एक test इस defect को स्पष्ट कर सकता है: B से A का session label replay करवाएं और साबित करें कि gateway फिर भी approval मांगता है। Client name display और diagnostics के लिए identifier है, यह प्रमाण नहीं कि वही executable दोबारा request कर रहा है।

## Per-call review credential के इस्तेमाल से जुड़ी होनी चाहिए

Per-call key setting का मतलब है कि gateway उस credential के हर इस्तेमाल पर consent मांगे। इसका मतलब «हर tool name के लिए एक बार», «हर open connection के लिए एक बार» या «जब तक client पुराना न हो तब तक एक बार» नहीं है। Review में वही वास्तविक request होनी चाहिए जो secret लेकर जाएगी या SSH key का इस्तेमाल करेगी।

ऐसे credential के साथ test बनाएं जिसे per-call review के लिए चिह्नित किया गया हो और ऐसा fixture रखें जो पहले से approved session में दो equivalent actions भेजे। HTTP के लिए किसी test endpoint पर दो calls करें जो harmless marker लौटाता हो। SSH के लिए किसी isolated test host पर दो harmless commands चलाएं। Test को दो अलग review events और दो अलग action records देखने चाहिए।

एक न्यूनतम expected event sequence ऐसी हो सकती है:

```text
session_authorized process=fixture-A
call_review_requested action=41 credential=deploy-token
call_completed action=41 result=success
call_review_requested action=42 credential=deploy-token
call_completed action=42 result=success
```

मुख्य assertion numbering नहीं है। मुख्य बात यह है कि action 42, action 41 के लिए मिले approval का इस्तेमाल न कर सके। दूसरा request पहले के पूरा होने के बाद भेजें, फिर ऐसा case बनाएं जिसमें दोनों लगभग एक ही समय पर आएं। Concurrency उन implementations को सामने लाती है जो किसी एक action से जोड़ने के बजाय session पर एक अस्थायी «review passed» bit रखती हैं।

अब यही test पुराने protocol declaration के साथ और उस capability को हटाकर दोहराएं जिसका इस्तेमाल आपका सामान्य approval-status path करता है। Local machine पर review का presentation अलग हो सकता है, लेकिन परिणाम वही रहना चाहिए। अगर user दूसरी review को dismiss, cancel या timeout करता है, तो test endpoint को दो नहीं, एक request दिखनी चाहिए।

Secret exposure पर hard assertion रखें। MCP response में action result, error या review-required state हो सकता है। उसमें API key, SSH private key, secret की जगह रखा गया redacted placeholder या ऐसा tool parameter नहीं होना चाहिए जिससे agent उसे फिर बना सके। Downgrade वह जगह है जहां compatibility adapter extra context serialize कर सकता है। केवल structured test object नहीं, raw transcript भी जांचें।

## Audit capture protocol negotiation के नीचे होना चाहिए

ऐसा audit journal जो केवल सफल modern-client calls रिकॉर्ड करता है, झूठा भरोसा देता है। बाद में आपको जिन cases की जरूरत पड़ेगी, वे हैं denied calls, canceled reviews, rejected initialization requests और वे अजीब compatibility paths जिन पर engineer ने कहा था, «ऐसा कभी नहीं होना चाहिए।»

Decision को फिर से समझने के लिए जरूरी facts रिकॉर्ड करें: agent run या session, action identity, channel, requested target, credential reference या safe identifier, authorization state, review outcome और execution outcome। Secret को खुद store न करें। Failed initialization के लिए इतना peer और parse context रिकॉर्ड करें जिससे rejection समझ में आए, लेकिन journal को untrusted payloads की copy न बनाएं।

Sallyport अपने Sessions और Activity journals को एक encrypted, hash-chained audit log से project करता है। Downgrade testing में यह design इसलिए महत्वपूर्ण है कि client capability यह तय न करे कि दूसरा journal बनेगा या नहीं। Current client, partial client और denied call, सभी को उसी write path से गुजरना चाहिए।

हर end-to-end case के बाद offline chain verification को एक assertion बनाएं:

```text
$ sp audit verify
verified: 18 records
chain: intact
```

Exact wording अलग हो सकती है, लेकिन test को vault खोले बिना सफल verification result आवश्यक बनाना चाहिए। यह test chained write के टूटे या छूटे होने को पकड़ता है। यह साबित नहीं करता कि सही event लिखा गया था, इसलिए इसे session और activity views की query के साथ जोड़ें और expected denied, approved, canceled या completed records की जांच करें।

Ordering का भी परीक्षण करें। अगर review deny होती है, तो journal में request और denial दिखनी चाहिए, लेकिन fabricated completion नहीं। अगर approval के बाद transport action विफल होती है, तो उसे execution failure के रूप में रिकॉर्ड करें, authorization denial के रूप में नहीं। ये अलग operational questions हैं। इन्हें मिलाने से incident review कठिन हो जाती है और टूटा हुआ adapter user choice जैसा दिख सकता है।

## Compatibility matrix को security outcomes के आधार पर बनाएं

हर version, capability, channel और credential setting का पूरा Cartesian product इतना बड़ा हो जाएगा कि कोई उसे चलाएगा नहीं। एक छोटी required matrix रखें जिसमें हर security decision शामिल हो। जब कोई नई adapter branch आए, तब एक नया case जोड़ें।

Initialization shape के लिए rows और local decision state के लिए columns रखें। उदाहरण के लिए, oldest-supported client, empty-capabilities client और normal current client को locked vault, unapproved session, session-approved credential और per-call-review credential के साथ चलाएं। अगर दोनों channels एक ही authorization path साझा करते हैं, लेकिन execution helpers अलग हैं, तो हर meaningful cell को HTTP और SSH दोनों से चलाएं।

Test चलने से पहले expected outcome को साधारण शब्दों में लिखें:

| Client condition | Local state | Expected result |
| --- | --- | --- |
| Oldest accepted version | Vault locked | Action denied and recorded |
| Empty capabilities | Vault unlocked, new process | Session approval requested and recorded |
| Older version | Approved process, per-call key | Per-call review requested for every action |
| Unsupported version | Any state | Initialization rejected, no outbound action |
| Current version | Revoked process | Action denied and recorded |

यह table इसलिए उपयोगी है क्योंकि यह unsupported versions के बारे में स्पष्ट decision लेने को मजबूर करती है। Negotiation विफल होने के बाद server को «अपनी तरफ से कोशिश» नहीं करनी चाहिए। इस वाक्य का अर्थ अक्सर यह होता है कि server ऐसे parser path पर request execute कर रहा है जिसकी supported path जितनी जांच नहीं हुई। Session reject करें, rejection को सुरक्षित तरीके से log करें और supported client मांगें।

हर row के लिए तीन स्वतंत्र observations इकट्ठी करें: raw MCP response, fake HTTP service या test SSH host पर observation और audit result। केवल error response यह साबित नहीं कर सकता कि कोई action बाहरी दुनिया तक नहीं पहुंचा। केवल test endpoint यह साबित नहीं कर सकता कि gateway ने denial रिकॉर्ड की। तीनों चाहिए।

## Failure transcript में टूटी हुई boundary का नाम होना चाहिए

जब compatibility test विफल हो, तो उसे «old MCP client has issues» के रूप में file न करें। यह description अगले developer को presentation ठीक करने और authorization miss करने की ओर ले जाएगी।

Invariant का नाम दें और timeline दिखाएं। यह ऐसा failure report है जिस पर काम किया जा सकता है:

```text
Client B initialized with oldest accepted version and no optional capabilities.
Client A had already received session approval and was still running.
Client B sent an HTTP action using the same displayed clientInfo name.
Gateway injected the credential and the test endpoint received the request.
No approval card appeared for B.
Activity journal recorded completion, but Sessions journal contained only A.
```

यह sequence बताता है कि defect process identity या authorization cache scope में है, version parsing में नहीं। यह भी पता चलता है कि audit log में ऐसी entry है जो investigator को गुमराह कर सकती है। Regression test इसी स्तर पर जोड़ें, फिर अंदर तक trace करें जब तक implementation में एक authorization check न हो जिसे B bypass न कर सके।

दूसरा failure pattern अधिक subtle है:

```text
Client initialized without the capability used for approval status updates.
Credential required per-call review.
First action displayed local review and completed.
Second action completed immediately.
Audit log recorded two completed actions and one review event.
```

यह बताता है कि review token अपने action scope से बाहर निकल गया। Fix पुराने client को रोकना नहीं है। Fix यह है कि approved review को action nonce या internal action record से जोड़ा जाए और ठीक एक बार consume किया जाए।

ये transcripts support teams को user decision और software failure में अंतर करने में भी मदद करते हैं। अगर journal कहता है कि user ने review deny की, तो request की जांच करें। अगर credential के लिए required review हुई ही नहीं, तो घटना को सामान्य agent behavior मानना बंद करें।

## Locking और revocation अलग-अलग tests हैं

Vault gate, session approval और per-call review अलग सवालों के जवाब देते हैं। Downgrade suite को इनका अलग-अलग परीक्षण करना चाहिए, क्योंकि सफल approval test vault-lock failure छिपा सकता है और इसका उलटा भी हो सकता है।

पहले vault lock करें। सबसे पुराने accepted version सहित हर compatibility fixture launch करें। हर protected action को credential injection से पहले विफल होना चाहिए और external service को कुछ भी नहीं दिखना चाहिए। ऐसा result स्वीकार न करें जिसमें agent से खुद credentials देने को कहा जाए। Agent को credentials कभी नहीं मिलने चाहिए।

फिर vault unlock करें, लेकिन session को unapproved रखें। Fixture को अपने run के लिए एक session-authorization path मिलना चाहिए। उसे approve करें, protected action चलाएं और process के जीवित रहते session revoke करें। Vault unlocked रहने पर भी अगली action विफल होनी चाहिए।

अंत में उसी approved session में per-call-review credential इस्तेमाल करें। एक action approve करें, अगली deny करें और journal जांचें। यह sequence साबित करता है कि decision ladder अपना क्रम बनाए रखती है: vault locked होने पर सभी actions रुकती हैं; session approval उस process के लिए authority स्थापित करती है; credential setting इसके ऊपर नया decision मांग सकती है।

इन tests को एक ही लंबे scenario में न समेटें, जब तक हर boundary के लिए focused tests भी मौजूद न हों। लंबे end-to-end tests यह साबित करने में अच्छे हैं कि कोई path मौजूद है। Refactor के बाद टूटने का कारण बताने में वे कमजोर होते हैं।

## Release gates को silent success पर रोक लगानी चाहिए

जब compatibility bugs किसी action को चुपचाप होने देते हैं, तो release रोकना चाहिए। Status message में मामूली mismatch अगली patch में ठीक किया जा सकता है। ऐसा downgraded client जो approval विरासत में ले, per-call review छोड़ दे, vault locked रहते काम करे या audit trail से गायब हो जाए, उसने security boundary पार कर ली है।

MCP initialization, protocol adapters, approval delivery, process tracking, credential injection या audit projection को छूने वाले हर बदलाव के लिए इन tests को continuous integration में mandatory बनाएं। Protocol-version bump का इंतजार न करें। Risk missing capability के आसपास बनाई गई branches से आता है और सामान्य feature work ऐसी branches लगातार बनाता रहता है।

जो test अक्सर सबसे अधिक लाभ देता है, वह आम तौर पर सबसे असुविधाजनक होता है: पुराने client declaration के साथ empty capability object, reused display name, दूसरा process और पहली approval के बाद credential का दूसरा इस्तेमाल। इसे बनाए रखें। इसी तरह वह friendly compatibility code मिलता है जिसने चुपचाप किसी मानवीय decision को cache hit में बदल दिया।
