وقتی ماشین‌ها با متن با یکدیگر صحبت می‌کنند؛ درسی از Fail2ban برای عصر Agentها

مقدمه یکی از ایده‌های ماندگار دنیای Unix این است که خروجی یک برنامه بتواند ورودی برنامه‌ای دیگر باشد. این ایده آن‌قدر ساده و در عین حال قدرتمند بود که بخش بزرگی از دنیای Unix و BSD بر پایه آن شکل گرفت: Program A → Output → Program B در بسیاری از موارد، این خروجی چیزی […]

مقدمه

یکی از ایده‌های ماندگار دنیای Unix این است که خروجی یک برنامه بتواند ورودی برنامه‌ای دیگر باشد.

این ایده آن‌قدر ساده و در عین حال قدرتمند بود که بخش بزرگی از دنیای Unix و BSD بر پایه آن شکل گرفت:

Program A → Output → Program B

در بسیاری از موارد، این خروجی چیزی جز متن نیست.

یک دستور وضعیت سیستم را نمایش می‌دهد، دستور دیگری آن را فیلتر می‌کند، ابزار بعدی بخشی از آن را استخراج می‌کند و در نهایت نتیجه به ابزار دیگری داده می‌شود.

این روش دهه‌هاست که کار می‌کند.

اما یک نکته مهم وجود دارد:

در بسیاری از این ارتباط‌ها، یک انسان در انتهای زنجیره حضور دارد.

انسان خروجی را می‌بیند، مفهوم آن را درک می‌کند و اگر چیزی تغییر کرده باشد، متوجه مشکل می‌شود.

امروز اما اتفاق متفاوتی در حال رخ دادن است.

در معماری‌های جدید مبتنی بر هوش مصنوعی، Agentها مستقیماً با Agentهای دیگر ارتباط برقرار می‌کنند؛ اغلب از طریق همان چیزی که برای انسان طراحی شده بود: متن.

و اینجا یک سؤال مهم مطرح می‌شود:

آیا متنی که برای انسان رابط بسیار خوبی است، الزاماً برای ارتباط ماشین با ماشین هم رابط مناسبی است؟

داستان Fail2ban می‌تواند پاسخ جالبی برای این سؤال داشته باشد.


یک مشکل کوچک در Fail2ban

در ۱۰ اوت ۲۰۲۴، یک Bug در بسته Fail2ban دبیان ثبت شد.

عنوان مشکل ساده بود:

فیلتر SSH با OpenSSH 9.8 کار نمی‌کند.

در نگاه اول ممکن است تصور کنیم چنین مشکلی باید باعث Crash شدن سرویس یا نمایش یک خطای واضح شود.

اما چنین اتفاقی نیفتاده بود.

OpenSSH همچنان کار می‌کرد.

Fail2ban نیز اجرا می‌شد.

لاگ‌های احراز هویت نیز وجود داشتند.

مشکل این بود که OpenSSH بخشی از پردازش خود را به فرآیند جدیدی به نام sshd-session منتقل کرده بود. در نتیجه نامی که در لاگ ثبت می‌شد تغییر کرده بود و الگوهایی که Fail2ban برای تشخیص ورود ناموفق استفاده می‌کرد، دیگر با آن لاگ‌ها مطابقت نداشتند.

نتیجه عجیب بود:

OpenSSH



Authentication Log



Fail2ban



تشخیص حمله

همه چیز ظاهراً سالم بود.

اما Fail2ban دیگر چیزی برای مسدود کردن پیدا نمی‌کرد.

این یعنی سیستم به جای اینکه بگوید:

«من این پیام را نمی‌فهمم.»

عملاً رفتار دیگری داشت:

«چیزی برای دیدن وجود ندارد.»

و این تفاوت بسیار مهم است.


چرا ارتباط متنی در Unix این‌قدر موفق بود؟

در سال ۱۹۷۸، Doug McIlroy یکی از اصول مهم Unix را مطرح کرد:

خروجی هر برنامه را طوری در نظر بگیرید که بتواند ورودی برنامه‌ای دیگر باشد.

این ایده یکی از پایه‌های فلسفه Unix شد.

برای مثال در یک سیستم Unix یا BSD می‌توان خروجی یک دستور را به ابزار دیگری داد:

command | grep something

یا:

command | awk '{print $1}'

یا:

command | sort

این مدل ساده، انعطاف‌پذیر و بسیار قدرتمند است.

در NetBSD نیز همین فلسفه را به‌وضوح می‌توان در ساختار ابزارهای پایه، Shell و مجموعه صفحات man مشاهده کرد. مستندات رسمی NetBSD مجموعه بزرگی از ابزارهای استاندارد سیستم، فرمان‌های مدیریتی و رابط‌های Unix را پوشش می‌دهد.

اما چرا متن جواب می‌دهد؟

چون معمولاً یک انسان در حلقه وجود دارد.

فرض کنید خروجی یک دستور تغییر کند.

ممکن است Script شما دیگر مقدار مورد انتظار را پیدا نکند.

یک انسان که خروجی را می‌بیند، احتمالاً متوجه تغییر خواهد شد و Script را اصلاح می‌کند.

به زبان ساده:

Machine → Text → Human

انسان می‌تواند متن را تفسیر کند.


Fail2ban چه چیزی را تغییر داد؟

Fail2ban یک قدم مهم‌تر برداشت.

این بار قرار نبود انسان لاگ‌ها را بخواند.

خود برنامه باید آنها را می‌خواند.

معماری تقریباً به این شکل است:

OpenSSH



Log



Fail2ban



تشخیص Login Failure



Block IP

ایده بسیار منطقی است.

هیچ مدیر سیستمی علاقه ندارد ساعت سه صبح بیدار شود و لاگ SSH را برای پیدا کردن حمله Brute Force بررسی کند.

بنابراین Fail2ban جای انسان را می‌گیرد.

اما اینجا یک مشکل اساسی ایجاد می‌شود.

OpenSSH برای انسان لاگ تولید می‌کند.

Fail2ban برای ماشین همان لاگ را می‌خواند.

و این دو الزاماً قرارداد رسمی و مشترکی درباره ساختار پیام ندارند.

در نتیجه Fail2ban باید حدس بزند OpenSSH یک اتفاق خاص را چگونه در قالب متن بیان می‌کند.

برای همین، فیلتر SSH آن مجموعه‌ای از Regular Expressionها دارد تا شکل‌های مختلف پیام‌های OpenSSH را تشخیص دهد.

در نسخه‌ای که نویسنده مقاله بررسی کرده، فیلتر SSH در FreeBSD 15 شامل ۲۷ عبارت Regular Expression بوده است.

مشکل دقیقاً همین‌جاست.

اگر OpenSSH شکل پیام را تغییر دهد:

Writer

تغییر متن


Log



Reader
X

ممکن است Reader دیگر پیام را تشخیص ندهد.

و مهم‌تر اینکه:

ممکن است هیچ خطایی هم رخ ندهد.


بدترین نوع خطا؛ خطایی که شبیه موفقیت است

فرض کنید Fail2ban یک لاگ را نمی‌شناسد.

از دید Fail2ban دو حالت وجود دارد:

حالت اول:
هیچ Login Failureای اتفاق نیفتاده است.

حالت دوم:
Login Failure اتفاق افتاده،
اما من نتوانستم لاگ آن را تشخیص دهم.

این دو حالت برای برنامه می‌توانند شبیه یکدیگر باشند.

این همان چیزی است که در سیستم‌های خودکار خطرناک می‌شود.

چون سیستم Crash نکرده است.

Error هم نداده است.

فقط کاری که باید انجام می‌داده انجام نداده است.


حالا Agentهای هوش مصنوعی را وارد ماجرا کنیم

حالا همین معماری را در یک سیستم Multi-Agent تصور کنید.

Agent اول یک وظیفه دریافت می‌کند.

پاسخ را به صورت متن تولید می‌کند.

Agent دوم آن متن را دریافت می‌کند و تصمیم می‌گیرد چه کاری انجام دهد.

بعد خروجی Agent دوم به Agent سوم داده می‌شود.

و همین‌طور ادامه پیدا می‌کند.

Agent A

Text


Agent B

Text

Agent C


Tool / API / System

اگر کمی دقت کنیم، ساختار کاملاً آشناست.

همان اجزای Fail2ban را داریم:

  • یک تولیدکننده؛

  • یک مصرف‌کننده؛

  • یک پیام متنی؛

  • نبود قرارداد کاملاً مشخص؛

  • و امکان شکست بدون Crash.

تفاوت مهم این است که Agentهای هوش مصنوعی می‌توانند یک مفهوم واحد را با عبارت‌های بسیار متفاوت بیان کنند.

مثلاً ممکن است Agent اول بگوید:

The firewall rule should block this address.

و در اجرای دیگر بگوید:

Please deny traffic originating from this IP.

و در اجرای سوم:

The source address must be rejected.

برای انسان، مفهوم تقریباً واضح است.

اما اگر برنامه‌ای قرار باشد این متن را Parse کند، وضعیت پیچیده‌تر می‌شود.


مشکل فقط Parsing نیست

در سیستم‌های Agentی، مسئله از Regular Expression پیچیده‌تر است.

یک Parser سنتی معمولاً مشخص می‌کند:

اگر الگو A را دیدی → عملیات X
اگر الگو B را دیدی → عملیات Y

اما مدل زبانی می‌تواند متن را تفسیر کند.

این انعطاف‌پذیری یک مزیت بزرگ است.

اگر شکل جمله کمی تغییر کند، مدل احتمالاً هنوز مفهوم آن را درک می‌کند.

اما همین ویژگی یک خطر جدید ایجاد می‌کند:

ممکن است مدل یک پیام را به شکلی کاملاً منطقی، اما اشتباه تفسیر کند.

یعنی خطا دیگر الزاماً واضح نیست.

ممکن است خروجی:

  • از نظر دستوری درست باشد؛

  • از نظر معنایی قابل قبول باشد؛

  • شبیه یک پاسخ واقعی باشد؛

  • اما با چیزی که Agent بعدی انتظار دارد متفاوت باشد.


این مشکل در پژوهش‌های Multi-Agent هم دیده شده است

در پژوهشی که در NeurIPS 2025 ارائه شد، پژوهشگران ۱۶۴۲ Execution Trace از هفت چارچوب Multi-Agent را بررسی و شکست‌ها را در ۱۴ دسته طبقه‌بندی کردند.

در سیستم‌های متن‌باز بررسی‌شده، نرخ شکست وظایف بین ۴۱ تا ۸۶٫۷ درصد گزارش شد.

یکی از دسته‌های مهم، Inter-Agent Misalignment بود؛ یعنی Agentها یکدیگر را اشتباه تفسیر می‌کردند یا در واقع درباره یک موضوع صحبت می‌کردند بدون اینکه معنای مشترکی از پیام داشته باشند.

این دسته ۳۲٫۳ درصد از شکست‌های گزارش‌شده را تشکیل می‌داد.

بنابراین مسئله فقط یک نگرانی نظری نیست.

ارتباط بین Agentها خودش می‌تواند محل شکست سیستم باشد.


راه‌حل چیست؟ Structured Output

یکی از پاسخ‌های مهم به این مشکل، استفاده از Structured Output است.

به جای اینکه به مدل بگوییم:

لطفاً پاسخ را در این قالب بنویس.

می‌توانیم یک Schema مشخص تعریف کنیم.

برای مثال:

{
  "action": "block_ip",
  "ip": "192.0.2.10",
  "duration": 3600
}

حالا مصرف‌کننده دقیقاً می‌داند چه چیزی دریافت کرده است.

در این حالت دیگر لازم نیست از روی جمله‌های مختلف حدس بزند که منظور مدل چیست.

داده دارای ساختار مشخص است.

در معرفی Structured Outputs در سال ۲۰۲۴، OpenAI نیز تفاوت قابل‌توجهی میان پیروی مدل از یک JSON Schema بدون اعمال اجباری Schema و حالتی که ساختار توسط پلتفرم enforce می‌شود گزارش کرد.


آیا باید متن را کنار بگذاریم؟

نه.

اتفاقاً اینجا باید مراقب یک اشتباه رایج باشیم.

مسئله این نیست که:

Text = Bad
JSON = Good

واقعیت پیچیده‌تر است.

زبان طبیعی مزیت بزرگی دارد.

دو سیستم که هیچ استاندارد مشترکی برای پیام‌های خود ندارند، ممکن است بتوانند از طریق زبان طبیعی با یکدیگر تعامل کنند.

این انعطاف‌پذیری بسیار ارزشمند است.

مدل زبانی همچنین می‌تواند تغییرات ظریف در بیان را بفهمد؛ کاری که یک Regular Expression نمی‌تواند انجام دهد.

بنابراین حذف متن راه‌حل مناسبی نیست.

راه‌حل بهتر این است که:

برای ارتباط ماشین با ماشین، یک قرارداد ساختاریافته داشته باشیم و متن را برای انسان حفظ کنیم.


تجربه FreeBSD و libxo

در اینجا یک نمونه جالب از دنیای BSD وجود دارد.

در FreeBSD کتابخانه‌ای به نام libxo برای همین مسئله طراحی شده است.

ایده ساده است:

یک برنامه باید بتواند خروجی خود را هم برای انسان و هم برای ماشین ارائه کند.

برای مثال:

netstat -rn

برای انسان خروجی متنی تولید می‌کند.

اما می‌توان از قابلیت libxo برای دریافت همان اطلاعات به صورت JSON استفاده کرد:

netstat --libxo json -rn

در این مدل، خروجی انسانی و خروجی ماشینی از یک منبع کد تولید می‌شوند. بنابراین خطر اینکه دو مسیر مستقل به مرور از یکدیگر فاصله بگیرند کمتر می‌شود.

معماری را می‌توان این‌طور تصور کرد:

              Application
                   │
          ┌────────┴────────┐
          │                 │
          ▼                 ▼
       Text              JSON
          │                 │
          ▼                 ▼
       Human             Machine

این ایده بسیار مهم‌تر از خود libxo است.


یک منبع اطلاعات؛ دو نوع خروجی

فرض کنید یک ابزار اطلاعات زیر را در اختیار دارد:

Interface: em0
Address: 192.168.1.10
Status: UP

برای انسان:

em0: 192.168.1.10 UP

کافی است.

اما برای یک برنامه بهتر است چیزی شبیه این دریافت شود:

{
  "interface": "em0",
  "address": "192.168.1.10",
  "status": "up"
}

نکته مهم این است که هر دو خروجی باید از یک منبع داده تولید شوند.

نه اینکه یک مسیر برای متن داشته باشیم و یک مسیر کاملاً جدا برای JSON.


مشکل ابزارهایی که بعداً JSON تولید می‌کنند

در دنیای Unix ابزارهایی وجود دارند که خروجی متنی برنامه‌های دیگر را بعداً Parse و به JSON تبدیل می‌کنند.

یکی از نمونه‌های شناخته‌شده jc است.

این ابزار در نسخه‌ای که نویسنده مقاله بررسی کرده، ۲۲۰ Parser داشته است.

چرا چنین تعداد زیادی Parser لازم است؟

چون بسیاری از ابزارهای Command Line از ابتدا برای خروجی ساختاریافته طراحی نشده‌اند.

در نتیجه معماری به این شکل درمی‌آید:

Application
    │
    ▼
Human-readable text
    │
    ▼
Parser
    │
    ▼
JSON

در حالی که معماری مطلوب‌تر این است:

                Application
                     │
              ┌──────┴──────┐
              ▼             ▼
             Text          JSON
              │             │
              ▼             ▼
            Human         Machine

تفاوت کوچک به نظر می‌رسد، اما از نظر معماری بسیار مهم است.


این موضوع برای NetBSD چه معنایی دارد؟

اینجا بحث برای ما در جامعه NetBSD جالب‌تر می‌شود.

NetBSD به شکل جدی بر ابزارهای استاندارد Unix، قابل‌حمل بودن، سادگی اجزای سیستم و مستندات دقیق تکیه دارد.

مستندات رسمی NetBSD مجموعه گسترده‌ای از ابزارهای پایه، سرویس‌ها، رابط‌های سیستم و صفحات man را ارائه می‌کند. حتی ساختار صفحات راهنما، فرمان‌ها را در بخش‌های مشخصی مانند General Commands، System Calls، File Formats، System Maintenance و Kernel Internals دسته‌بندی می‌کند.

این همان دنیایی است که فلسفه Pipeline و Text Interface در آن شکل گرفته است.

اما عصر Agentها سؤال جدیدی ایجاد می‌کند:

اگر یک ابزار NetBSD قرار باشد خروجی خود را مستقیماً در اختیار یک برنامه دیگر، Orchestrator یا Agent قرار دهد، آیا خروجی متنی کافی است؟

پاسخ احتمالاً این است:

برای انسان، بله؛ برای ماشین، همیشه نه.


یک مسیر منطقی برای ابزارهای آینده BSD

تصور کنید ابزارهای سیستم عامل بتوانند از ابتدا چند نوع خروجی داشته باشند:

mytool

خروجی مناسب انسان:

Interface em0 is UP
Address: 192.168.1.10

و:

mytool --json

خروجی مناسب ماشین:

{
  "interface": "em0",
  "status": "up",
  "address": "192.168.1.10"
}

و حتی:

mytool --yaml

برای محیط‌هایی که YAML ترجیح داده می‌شود.

این مدل با فلسفه Unix در تضاد نیست.

برعکس، می‌تواند ادامه طبیعی همان فلسفه باشد:

خروجی یک برنامه باید بتواند ورودی برنامه دیگری باشد.

فقط اکنون می‌دانیم که «برنامه دیگر» الزاماً یک ابزار ساده Unix نیست.

ممکن است یک API، یک Orchestrator یا یک Agent هوش مصنوعی باشد.


متن هنوز برای انسان ضروری است

یک نکته مهم را نباید فراموش کنیم.

اگر همه چیز را به Schema تبدیل کنیم، ممکن است سیستم برای ماشین بسیار مناسب باشد اما برای مدیر سیستم بسیار سخت شود.

فرض کنید یک زنجیره طولانی از Agentها فقط JSON تولید کند.

وقتی مشکلی رخ دهد، مدیر سیستم باید بتواند بفهمد چه اتفاقی افتاده است.

بنابراین سیستم مناسب باید هر دو را داشته باشد:

                 System
                   │
          ┌────────┴────────┐
          │                 │
          ▼                 ▼
   Structured Data        Text
          │                 │
          ▼                 ▼
       Machine             Human

این شاید مهم‌ترین درس معماری libxo باشد:

ساختار مناسب برای ماشین را اضافه کنیم، اما رابط انسانی را حذف نکنیم.


از Unix تا Agent

اگر به تاریخ Unix نگاه کنیم، یک مسیر جالب دیده می‌شود.

در ابتدا:

Program → Text → Human

سپس:

Program → Text → Program

و اکنون:

Agent → Text → Agent → Tool → Agent

در مدل قدیمی Unix، انسان معمولاً می‌توانست خطا را ببیند.

در سیستم‌های جدید، ممکن است هیچ انسانی در این زنجیره حضور نداشته باشد.

و این تفاوت بسیار مهم است.

وقتی انسان حذف می‌شود، باید چیزی جایگزین توانایی او برای تشخیص ابهام شود:

قرارداد مشخص، Schema، اعتبارسنجی و خطاهای قابل تشخیص.


درس Fail2ban برای توسعه‌دهندگان NetBSD

Fail2ban یک مثال کوچک است، اما درس آن بزرگ است.

مشکل Fail2ban این نبود که Regular Expression بد است.

مشکل این بود که:

Program A
   │
Text intended for humans ▼ Program B

بدون اینکه قرارداد محکمی درباره معنای این متن وجود داشته باشد.

در دنیای Agentها همین الگو با مقیاس بسیار بزرگ‌تر تکرار می‌شود.

بنابراین شاید یکی از اصول مهم سیستم‌های آینده این باشد:

اگر خروجی یک برنامه قرار است توسط برنامه‌ای دیگر مصرف شود، خروجی ساختاریافته باید بخشی از Interface رسمی آن برنامه باشد، نه چیزی که بعداً از روی متن استخراج شود.

و در کنار آن:

اگر انسان هم قرار است خروجی را بخواند، خروجی متنی مناسب انسان همچنان باید حفظ شود.


جمع‌بندی

فلسفه Unix یک ایده ساده اما ماندگار داشت:

یک برنامه کوچک بساز.
کارش را خوب انجام بده.
و خروجی آن را طوری ارائه کن که بتواند در اختیار ابزار دیگری قرار گیرد.

متن برای دهه‌ها بهترین راه تحقق این ایده بود.

و هنوز هم هست؛ مخصوصاً وقتی یک انسان در انتهای زنجیره حضور دارد.

اما دنیای امروز در حال تغییر است.

Agentها به جای انسان، خروجی Agentهای دیگر را می‌خوانند.

در چنین محیطی، دیگر نمی‌توان تنها به این امید تکیه کرد که مصرف‌کننده «منظور» تولیدکننده را از متن بفهمد.

ما به قراردادهای ساختاریافته نیاز داریم.

در عین حال، نباید انسان را از Interface حذف کنیم.

بهترین معماری احتمالاً این نیست:

Text OR JSON

بلکه:

             یک منبع داده
                  │
          ┌───────┴───────┐
          ▼               ▼
       Human             Machine
          │               │
        Text        Structured Data

این شاید شکل جدید همان فلسفه‌ای باشد که Unix دهه‌ها پیش بنا کرد.

متن همچنان رابطی عالی میان انسان و ماشین است؛ اما وقتی ماشین‌ها قرار است با یکدیگر صحبت کنند، بهتر است به جای حدس زدن معنای متن، یک قرارداد مشخص برای داده داشته باشند.

و شاید مهم‌تر از همه:

وقتی انسان را از زنجیره ارتباط حذف می‌کنیم، باید Interface را دقیق‌تر کنیم؛ نه اینکه فقط امیدوار باشیم ماشین بعدی منظور ماشین قبلی را بفهمد.

پیمایش به بالا