# आंशिक SSH कमांड विफलताएं: एजेंट को काम दोहराने से रोकें

AI एजेंट को विफल SSH कमांड को अज्ञात state transition मानना चाहिए, उसी text को दोबारा चलाने की अनुमति नहीं। कोई कमांड user बना सकती है, service reload कर सकती है और फिर अंतिम फ़ाइल लिखते समय विफल हो सकती है। बिना जांच की रिट्राई duplicate account बना सकती है, हाथ से बदली गई setting को overwrite कर सकती है या recover किए जा सकने वाले deployment को outage में बदल सकती है।

आम सलाह, «exit code जांचें», जरूरी है, लेकिन अधूरी है। Exit status बताता है कि process कैसे समाप्त हुआ। रिकवरी के लिए यह रिकॉर्ड चाहिए कि process ने क्या पूरा किया, host अब कौन सी स्थिति बता रहा है और एजेंट कौन सा operation करना चाहता था। इन तथ्यों को durable command receipt में रखें, फिर एजेंट को दोबारा कार्रवाई से पहले reconciliation करने दें।

## SSH विफलता तीन अलग तरह की अनिश्चितताएं छोड़ती है

विफल SSH action का अर्थ हो सकता है कि remote shell विफल हुई, connection टूट गया या controller ने इंतजार करना बंद कर दिया। इन तीनों स्थितियों का इलाज अलग है, लेकिन एजेंट अक्सर सभी को «command failed» में बदल देते हैं।

मान लें कि remote deployment script ये काम इसी क्रम में करती है:

1. यह नई application archive को release directory में लिखती है।
2. यह `current` symlink को उस release पर बदलती है।
3. यह service को restart करती है।
4. यह health check चलाती है और temporary dependency error मिलने पर nonzero लौटाती है।

Command failure लौटाने के बावजूद release live है। हर operation दोहराव सहन करता हो तो script दोहराना सुरक्षित हो सकता है। अक्सर ऐसा नहीं होता। Script नई release directory बना सकती है, log को truncate कर सकती है, credential rotate कर सकती है या migration चला सकती है। केवल exit code से यह तय नहीं हो सकता कि क्या हुआ।

दूसरी स्थिति और खराब है: SSH transport interruption। Network टूटने पर local process को 255 मिल सकता है, जबकि remote shell चलती रहती है। Controller timeout में भी यही समस्या है। Controller को सिर्फ इतना पता है कि उसे अंतिम उत्तर नहीं मिला। उसे यह नहीं पता कि target को request मिली, shell शुरू हुई या process अभी बदलाव कर रहा है।

यह अंतर एजेंट की अगली कार्रवाई बदल देता है:

- Confirmed remote exit और receipt होने पर failed checkpoint के आधार पर recovery करें।
- Transport failure पर कोई mutation करने से पहले state देखें।
- Controller timeout पर state देखें और जरूरत हो तो duplicate request के बजाय स्पष्ट cancellation procedure चलाएं।

तीनों को retry न कहें। Retry वह operation है जिसके दोहराव का सुरक्षित नियम ज्ञात हो। अज्ञात remote state को reconciliation चाहिए।

## Exit status process बताता है, transaction नहीं

SSH exit code उपयोगी evidence देता है, लेकिन यह database commit record नहीं है। OpenSSH `ssh` manual के अनुसार `ssh` remote command का exit status लौटाता है, या error होने पर 255 लौटाता है। यह सीमा automation systems अक्सर अनदेखी कर देते हैं: remote exit status उस command का विवरण देता है जो SSH को मिला, जबकि 255 SSH के अपने error path को दिखाता है।

Zero exit code की भी व्याख्या जरूरी है। POSIX shell में साधारण sequential list का status आम तौर पर अंतिम command से आता है। यह script किसी महत्वपूर्ण failure के बाद भी success बता सकती है:

```sh
install -m 0644 app.conf /etc/myapp/app.conf
systemctl restart myapp
logger -t deploy "deployment finished"
```

अगर `install` विफल हो, लेकिन `systemctl restart` और `logger` zero लौटाएं, तो script का अंतिम status zero होगा। एजेंट success देखेगा और गलत तरीके से कह सकता है कि configuration बदल गई। अंतिम `echo done` भी यही भ्रम पैदा करता है।

Pipelines एक और failure path जोड़ती हैं। Bash Reference Manual के अनुसार Bash में `pipefail` सक्षम न हो तो pipeline का status अंतिम command का status होता है। इस command का परिणाम zero हो सकता है, अगर extraction उपयोगी data मिले बिना cleanly exit कर जाए:

```bash
curl --fail --silent https://example.invalid/build.tar.gz | tar -xz -C /srv/myapp
```

स्पष्ट interpreter इस्तेमाल करें और जरूरी behavior घोषित करें:

```bash
#!/usr/bin/env bash
set -Eeuo pipefail

curl --fail --silent --show-error "$archive_url" | tar -xz -C "$release_dir"
```

`-e` Bash को कई unhandled failures पर रोकता है, `-u` unset variables को अस्वीकार करता है और `pipefail` pipeline के पहले हिस्सों की विफलता को सुरक्षित रखता है। `E` option से `ERR` trap functions और command substitutions के भीतर भी लागू हो सकता है। ये settings failure reporting बेहतर करती हैं। वे sequence को atomic नहीं बनातीं।

यह बात महत्वपूर्ण है। `set -e` किसी operation के error लौटाने के बाद चलता है। इससे पहले command द्वारा बनाई गई directory हटाई नहीं जा सकती और पहले restart की गई service वापस नहीं लाई जा सकती। इसके कुछ ऐसे exceptions भी हैं जो लोगों को चौंकाते हैं: `if` से जांची गई commands, `&&` या `||` के बाईं ओर की commands और कई compound contexts हमेशा exit नहीं कराते। जिन actions की failure recovery बदलती है, उनके आसपास explicit checks लिखें।

## Command लिखने से पहले operation boundary तय करें

एजेंट «service deploy करो» जैसे अस्पष्ट निर्देश को recover नहीं कर सकता। Remote command को ऐसे नामित operation की जरूरत है जिसका postcondition observer जांच सके।

Release change के लिए operation हो सकता है: «`/srv/myapp/current` को release `2025-04-18.3` पर सेट करो, फिर पुष्टि करो कि active service वही release बता रही है।» Database change के लिए: «`add_invoice_index` migration को ठीक एक बार लागू करो और पुष्टि करो कि उसका migration record मौजूद है।» Command text implementation detail है। Operation और उसका postcondition तय करते हैं कि recovery आगे बढ़ सकती है या नहीं।

Irreversible या externally visible boundaries पर operation को विभाजित करें। हर shell line के बाद checkpoint जरूरी नहीं। State change के बाद checkpoint दर्ज करें, अगर उससे अगला निर्णय बदलता है। Deployment में archive verified, release directory populated, symlink switched, service restarted और health state observed जैसे checkpoints हो सकते हैं।

हर remote command को «idempotent» बनाने और फिर हमेशा retry करने की लोकप्रिय लेकिन गलत सलाह से बचें। Idempotence किसी खास operation और घोषित desired state पर निर्भर करती है। `mkdir -p /srv/app` दोहराने पर सुरक्षित हो सकती है। `useradd deploy` तभी दोहराने योग्य है जब agent यह जांचे कि मौजूदा account का UID, group, home directory और shell अपेक्षित हैं। `ALTER TABLE` दूसरी बार विफल हो सकता है या खराब migration संबंधित बदलाव दो बार कर सकती है।

कोई command दोहराने पर सुरक्षित हो सकती है, लेकिन उसके आसपास का workflow सुरक्षित न हो। Service restart दोहराने योग्य हो सकता है, मगर configuration copy के बीच restart करने पर अधूरी file खुल सकती है। State validation को action के पास रखें। एजेंट से generic rule के आधार पर इसका अनुमान न लगवाएं।

Access देने से पहले हर operation के लिए चार fields तय करें:

- ऐसा operation ID जो recovery के दौरान वही रहे।
- ऐसा desired postcondition जिसे read-only command जांच सके।
- पूरे हुए state changes बताने वाले checkpoints।
- हर अधूरे checkpoint के लिए recovery action।

Operation ID केवल नाम भर नहीं है। अगर controller हर attempt पर नया identifier बनाए, तो target continuation और नए request में फर्क नहीं कर पाएगा। इसी तरह repeated migrations और duplicate provisioning होते हैं।

## हर state change से पहले और बाद receipt लिखें

Durable receipt partial failure को inspect किए जा सकने वाले event में बदल देती है। पहली mutation से पहले target host पर receipt लिखें, हर meaningful checkpoint के बाद उसे update करें और updates को atomic बनाएं।

नीचे दी गई Bash script जानबूझकर सरल है। यह पहले से staged release को symlink बदलकर और system service restart करके deploy करती है। यह हर deployment method का समाधान होने का दावा नहीं करती। इसका उद्देश्य agent के लिए जरूरी receipt mechanics दिखाना है।

```bash
#!/usr/bin/env bash
set -Eeuo pipefail

operation_id=${1:?operation ID required}
release=${2:?release path required}
service=${3:?service name required}
state_dir=/var/lib/agent-ops
receipt="$state_dir/$operation_id.receipt"
tmp="$receipt.$$"

mkdir -p "$state_dir"
chmod 0700 "$state_dir"

write_receipt() {
  cat >"$tmp" <<EOF
operation_id=$operation_id
release=$release
service=$service
checkpoint=$1
updated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
EOF
  chmod 0600 "$tmp"
  mv -f "$tmp" "$receipt"
}

fail() {
  status=$?
  write_receipt "failed:$status"
  exit "$status"
}
trap fail ERR

if [[ -f "$receipt" ]]; then
  . "$receipt"
  case "$checkpoint" in
    complete)
      printf 'operation already complete: %s\n' "$operation_id"
      exit 0
      ;;
    switched|restarted)
      printf 'operation requires reconciliation: %s\n' "$checkpoint" >&2
      exit 75
      ;;
  esac
fi

[[ -d "$release" ]]
write_receipt "release_verified"

ln -sfn "$release" /srv/myapp/current
write_receipt "switched"

systemctl restart "$service"
write_receipt "restarted"

active_target=$(readlink -f /srv/myapp/current)
[[ "$active_target" == "$release" ]]
systemctl is-active --quiet "$service"
write_receipt "complete"
printf 'operation complete: %s\n' "$operation_id"
```

Temporary file और `mv` महत्वपूर्ण हैं। एक ही filesystem पर rename एक operation में receipt को replace करता है, इसलिए reader को आधी file के बजाय या तो पुरानी complete receipt या नई complete receipt मिलती है। State directory को केवल operation के owner account के लिए writable रखें। अगर कोई untrusted user receipts बदल सकता है, तो agent की recovery logic evidence के रूप में झूठ स्वीकार कर लेगी।

`ERR` trap Bash द्वारा failure संभालते समय exit status दर्ज करती है। Machine power खो दे, uncatchable signal मिले या process अचानक मर जाए तो trap नहीं चल सकती। इसलिए script केवल अंतिम trap पर निर्भर रहने के बजाय हर completed state change के बाद progress दर्ज करती है।

इस छोटे example की तरह arbitrary receipt formats को source न करें, जब तक directory का ownership और permissions सख्त न हों। Production में किसी known parser से JSON पढ़ें या ऐसा fixed line format इस्तेमाल करें जो अनजान fields को अस्वीकार करे। Example केवल protected directory में अभी बनाई गई file को source करता है, ताकि shell code पढ़ने योग्य रहे।

Receipt में optimistic intent नहीं, observed facts होने चाहिए। `checkpoint=switched` का अर्थ है कि symlink command सफलतापूर्वक लौटी। इसका अर्थ यह नहीं कि service ने नई release load कर ली। `complete` explicit postcondition checks के बाद आता है। यह फर्क agent को लिखी हुई command को completed operation मानने से रोकता है।

## Agent से नई command नहीं, reconciliation करवाएं

Nonzero result के बाद agent को original operation ID सुरक्षित रखकर पहले read-only checks करने चाहिए। उसे मामूली wording बदलकर deployment command फिर से नहीं बनानी चाहिए। नई wording नया state transition पैदा नहीं करती।

ऊपर की deployment receipt के लिए reconciliation command durable record और live postcondition दोनों जांच सकती है:

```bash
operation_id='release-7f3b'
cat "/var/lib/agent-ops/$operation_id.receipt"
printf 'current='
readlink -f /srv/myapp/current
systemctl is-active myapp
systemctl show myapp --property=ActiveState --property=SubState --no-pager
```

Output ऐसा होना चाहिए जिसे agent prose को evidence मानने का दिखावा किए बिना parse कर सके:

```text
operation_id=release-7f3b
release=/srv/myapp/releases/2025-04-18.3
service=myapp
checkpoint=restarted
updated_at=2025-04-18T14:05:12Z
current=/srv/myapp/releases/2025-04-18.3
active
ActiveState=active
SubState=running
```

यहां symlink requested release पर है और service active है, लेकिन receipt `restarted` पर रुक गई। Service restart होने के बाद और `complete` लिखे जाने से पहले remote process मर गया हो सकता है। Recovery procedure postcondition checks फिर चला सकती है और वे सफल हों तो सीमित recovery command के जरिए completion receipt लिख सकती है। उसे deployment को शुरुआत से नहीं चलाना चाहिए।

एक उपयोगी controller protocol request, result और recovery को अलग रखता है। उदाहरण के लिए:

```json
{
  "operation_id": "release-7f3b",
  "action": "deploy_release",
  "target": "app-01",
  "arguments": {
    "release": "/srv/myapp/releases/2025-04-18.3",
    "service": "myapp"
  },
  "mode": "reconcile"
}
```

Target को `mode: reconcile` केवल read-only inspection या ऐसे पहले से लिखे completion path के लिए स्वीकार करना चाहिए जो postcondition की पुष्टि करता हो। Agent को `reconcile` के नाम पर arbitrary shell string न भेजने दें। जब command कुछ भी mutate कर सकती हो, तो उस label का कोई security अर्थ नहीं रहता।

Sample में exit code 75 जानबूझकर temporary-failure signal है। Exact number से ज्यादा जरूरी documented contract है: agent समझे कि उसे state देखनी है, automatic retry submit नहीं करनी। Controller outcomes में `completed`, `reconcile_required`, `rejected_before_start` और `transport_unknown` के लिए अलग परिणाम रखें। केवल Boolean `success` field recovery के लिए जरूरी जानकारी मिटा देती है।

## Timeout और disconnect में remote state का प्रमाण चाहिए

Timeout local observation है। Local client ने इंतजार छोड़ दिया, जरूरी नहीं कि remote command भी रुक गई हो। Timeout को cancellation मानना remote action duplicate करने के सबसे तेज तरीकों में से एक है।

Controller operations को single-flight बनाकर अस्पष्टता घटा सकता है। Mutation से पहले remote script operation ID से जुड़ा exclusive lock बनाए। अगली attempt lock देखे और waiting, process inspection या human decision की जरूरत बताने में से चुने।

Simple host-level lock के लिए `flock` अक्सर पर्याप्त है:

```bash
exec 9>/var/lib/agent-ops/deploy.lock
if ! flock -n 9; then
  printf 'another deployment operation is active\n' >&2
  exit 75
fi
```

यह सुरक्षा केवल उन processes पर लागू होती है जो उसी lock का सम्मान करते हैं। यह अलग deployment procedure चलाने वाले administrator से नहीं बचाती और script की हर design error को भी नहीं रोकती। Database migration के लिए जहां उपलब्ध हो, database का advisory lock या migration lock इस्तेमाल करें। API action के लिए उस API द्वारा समर्थित idempotency token इस्तेमाल करें। Lock को उसी state के पास रखें जिसकी वह रक्षा करती है।

Timeout के बाद controller reconnect करे तो इस क्रम में जांच करें:

1. Original operation ID की receipt पढ़ें।
2. अगर operation में भरोसेमंद process marker है, तो original process अभी चल रहा है या नहीं देखें।
3. Read-only commands से operation का postcondition जांचें।
4. स्पष्ट recovery action चुनें या observations असहमत हों तो मामला escalate करें।

Process मौजूद होना अकेला signal न बनाएं। Process किसी external dependency पर अटका हो सकता है और process का न होना यह नहीं बताता कि समाप्त होने से पहले उसने क्या बदला। Receipt और postcondition ज्यादा मजबूत evidence देते हैं।

SSH multiplexing में भी यही सावधानी चाहिए। Master connection shared transport के पीछे individual command की failure छिपा सकता है और controller closed channel को failed operation समझ सकता है। Remote command का stdout, stderr, raw SSH exit status, start time और end time एक action record में capture करें। stderr सुरक्षित रखें, भले agent उसका summary बना दे। Raw data अक्सर दिखाता है कि Bash ने unset variable अस्वीकार किया, remote command ने 75 लौटाया या SSH ने खुद 255 लौटाया।

## कुछ बदलावों को retry नहीं, compensation चाहिए

कई operations बाद में सुरक्षित रूप से repeatable नहीं बनाए जा सकते। Credential rotation, destructive cleanup, payment जैसे API calls और schema migrations के लिए compensating procedure या operator decision चाहिए।

Credential rotation workflow लें। Command नई credential बना सकती है, service update कर सकती है, access verify कर सकती है और फिर पुरानी credential revoke कर सकती है। अगर creation के बाद और service update से पहले failure हो जाए, तो retry एक और credential बना सकती है और कई live secrets छोड़ सकती है। Receipt में नई बनाई गई credential का identifier तुरंत दर्ज करें। Recovery तब जांच सकती है कि service कौन सी credential इस्तेमाल कर रही है और तय कर सकती है कि नई credential update, revoke या retain करनी है।

Receipt, standard output या agent context में secret material न रखें। केवल nonsecret identifier या fingerprint रखें, बशर्ते वह identifier खुद access न देता हो। Recovery process को यह जानना है कि कौन सा object मौजूद है, उसका private value नहीं।

Database migrations में अलग समस्या होती है। Migration framework की history table बता सकती है कि नामित migration पूरी हुई, लेकिन framework transaction के बाहर चला interrupted data backfill उसमें दर्ज नहीं हो सकता। Schema change, backfill progress और completion marker के लिए अलग checks लिखें। अगर database आपके operation के लिए transactional DDL support करता है तो उसका इस्तेमाल करें, लेकिन यह न मानें कि हर DDL statement या external side effect transaction के साथ rollback होगा।

Externally visible actions के लिए SSH-based अनुमान के बजाय API idempotency token को प्राथमिकता दें। Remote host जिस API को call करता है, अगर वह idempotency key स्वीकार करती है तो request से पहले token को receipt में सुरक्षित रखें। Recovery में उसी token से API query करें या API के documented semantics के अनुसार उसी token के साथ resubmit करें। हर agent attempt पर नया token बनाना इस सुविधा को निष्प्रभावी कर देता है।

नियम सीधा है: अगर आप यह नहीं बता सकते कि किसी action के होने का पता कैसे चलेगा, तो autonomous agent को उसे retry करने की अनुमति न दें। Target का निरीक्षण करने के लिए human से कहें या operation को durable state record के आधार पर फिर design करें।

## Shell scripts को ऐसा contract चाहिए जिसे agent लागू कर सके

Agent के लिए बनाई गई script को संकीर्ण, machine-readable contract देना चाहिए। Chatty logs, रंगीन terminal output और warnings तथा success messages के मिश्रण से state समझना agents के लिए कठिन होता है।

Stable exit categories, एक result object और स्पष्ट operation IDs इस्तेमाल करें। उदाहरण के लिए, outcome पता चलने के बाद ही अंतिम JSON line लिखें:

```json
{"operation_id":"release-7f3b","outcome":"reconcile_required","checkpoint":"switched","exit_code":75}
```

सामान्य diagnostic output stderr पर रखें और stdout को result record के लिए सुरक्षित रखें, अगर controller इस convention को लागू कर सकता है। Banners, progress bars और JSON को एक ही stdout पर भेजने वाली shell command parser failures को आमंत्रित करती है। अगर command उपयोगी progress stream करती है, तो पहले durable receipt लिखें और controller को final structured record को सुविधा मानने दें, एकमात्र record नहीं।

Agent को arbitrary checkpoint names, receipt paths, service names या interpreters चुनने न दें। Reviewed command को constrained arguments के साथ expose करें। `--` के बाद free-form shell स्वीकार करने वाला wrapper समस्या को केवल साफ label के पीछे छिपाता है।

Command contract में यह भी लिखा होना चाहिए कि कौन सी failures retry के लिए सुरक्षित हैं। Package download host में बदलाव से पहले retryable network error लौटा सकता है। Symlink switch के बाद lost connection को link target की reconciliation से पहले retry नहीं किया जा सकता। यह classification operation author को करनी चाहिए, जो effects समझता है, न कि stderr देखकर अनुमान लगाने वाले model को।

Test host इस्तेमाल करें और हर checkpoint पर script को interrupt करें। Archive extraction के दौरान, symlink switch के बाद, restart के दौरान और अंतिम health check के बाद termination signal भेजें। फिर reconciliation path चलाकर देखें कि सही निर्णय मिलता है या नहीं। अगर आपने जानबूझकर किसी operation को कभी interrupt नहीं किया, तो आपको नहीं पता कि उसका retry behavior सुरक्षित है या नहीं।

## Authorization और audit records को recovery story सुरक्षित रखनी चाहिए

Agent को recovery read करने का अधिकार चाहिए, लेकिन read के बाद होने वाली mutation को भी सामान्य authorization boundary से गुजरना चाहिए। `status` नाम की command के भीतर दूसरा deployment न छिपाएं।

Sallyport SSH credential को agent से बाहर रख सकता है और agent run तथा हर action दोनों दर्ज करता है। इससे यह पता रखने में मदद मिलती है कि recovery attempt को किसने authorize किया और कौन सी command भेजी गई। इसका audit trail target-side receipt की जगह नहीं लेता: audit record साबित कर सकता है कि action request हुई, जबकि receipt और postcondition target की resulting state समझाते हैं।

Incident notes में इन records को अलग रखें। Session identity बताती है कि किस agent process के पास permission थी। Individual action record बताती है कि किस host पर कौन सी command चली और उसने क्या लौटाया। Target receipt बताती है कि operation कहां रुकी। Deployment गलत होने पर इन तथ्यों को एक chat transcript में मिला देना जरूरी evidence गंवा देता है।

Irreversible effects वाले operations के लिए per-call confirmation दें, खासकर जब reconciliation से credential deletion, schema repair या cleanup हो सकता हो। Agent के evidence में विरोध हो तो यह अतिरिक्त approval उपयोगी है। उदाहरण के लिए receipt कहती है कि switch पूरा हो गया, लेकिन active service अब भी पुरानी release बता रही है। यह decision point है, routine retry नहीं।

अच्छा recovery design conservative action को आसान बनाता है। Agent को read-only reconciliation command, durable operation ID और तय escalation result दें। तब disconnect दूसरी बार system बदलने वाली attempt के बजाय inspectable record बनता है।

## उस command को ठीक करने से शुरू करें जिसे लोग पहले से दोहराते हैं

वह SSH command खोजें जिसे आपकी team timeout या लाल deployment message के बाद फिर चलाती है। कुछ और बदलने से पहले उसमें operation ID, protected receipt और एक read-only postcondition check जोड़ें।

फिर उसे जानबूझकर interrupt करें। अगर recovery path यह नहीं बता सकती कि पहली attempt ने state बदली या नहीं, तो वह autonomous agent के लिए तैयार नहीं है। Operation को तब तक दोबारा लिखें जब तक उत्तर exit code पर भरोसे से नहीं, target host से न मिले।
