Jalankan Perintah Windows dari WSL2 Tanpa SSH
Pola drop-file queue: cara AI agent di container WSL2 menjalankan perintah di host Windows tanpa SSH, lengkap dengan whitelist command dan audit log.
AI agent saya hidup di container Docker di dalam WSL2. Docker Desktop, Windows Task Scheduler, dan beberapa tooling ada di host Windows. Untuk menjalankan operasi tertentu — restart container, deploy aplikasi, reboot Docker Desktop — agent butuh cara mengeksekusi perintah di host Windows dari dalam container. Pola yang saya pakai: drop-file queue bridge untuk menjalankan perintah Windows dari WSL2 tanpa SSH.
Ini artikel tentang memberi program otonom akses ke host. Pola ini bekerja, dan ini cara saya membatasinya supaya tidak meledak.
Peringatan keamanan: Pola ini hanya aman kalau whitelist ketat, path dibatasi, dan folder queue tidak bisa ditulis oleh proses lain. Jangan implementasikan tanpa whitelist. Bagian implementasi di bawah menjelaskan setiap lapisan pembatas yang saya pasang.
TL;DR:
- Untuk menjalankan perintah Windows dari WSL2 tanpa SSH, agent menulis file
.cmdke folder shared yang dibaca watcher PowerShell di host - Whitelist berbasis daftar-izin (bukan larangan) + filter shell metacharacters membatasi blast radius ke perintah yang di-whitelist saja
- Setiap perintah tercatat di audit log dengan timestamp dan exit code
- Trade-off: latency polling ~60 detik dan tanpa streaming output — acceptable untuk operasi infra, salah untuk operasi interaktif
Kenapa saya tidak pakai SSH
Empat opsi untuk menjalankan perintah di host Windows dari container: SSH server di Windows, Docker socket di-mount, named pipe, dan drop-file queue. Setelah mencoba dan mengaudit semuanya, berikut perbandingannya:
SSH server di Windows. Setup effort: tinggi (install OpenSSH Server, butuh admin). Blast radius kalau agent salah: besar — akses shell penuh ke seluruh host. Auditability: sedang (log auth, tapi perintah tidak terstruktur). Portabilitas: rendah (butuh konfigurasi per host). Windows OpenSSH Server juga butuh elevation untuk install di mesin ini.
Docker socket di-mount. Setup effort: rendah (mount /var/run/docker.sock). Blast radius: sangat besar — siapapun yang bisa akses socket bisa menjalankan container apa pun, termasuk yang mount filesystem host. Auditability: rendah (tidak ada log perintah). Portabilitas: tinggi.
Named pipe. Setup effort: sedang. Blast radius: sedang. Auditability: rendah. Named pipe Windows tidak straightforward dari Docker container — butuh konfigurasi network yang kompleks.
Drop-file queue. Setup effort: sedang (folder shared + watcher script). Blast radius: kecil — hanya perintah yang di-whitelist yang bisa dieksekusi. Auditability: tinggi — setiap perintah tercatat di log dengan timestamp, exit code, dan output. Portabilitas: tinggi (cuma butuh folder shared + scheduled task).
Kesimpulan jujur: queue file menang di blast radius dan auditability, kalah di latency (60 detik polling vs real-time SSH). Untuk operasi infra (deploy, restart, rebuild), latency 60 detik tidak masalah. Untuk operasi interaktif, pola ini salah pilihan.
Cara kerja pola drop-file queue untuk menjalankan perintah Windows dari WSL2
Agent (container)
↓ tulis <id>.cmd ke /hermes-queue/
/hermes-queue/ (= C:\hermes-ops\queue\ di Windows)
↓ watcher PowerShell (hermes-bridge.ps1) poll setiap ~60s
↓ baca file .cmd, validasi whitelist + reject metacharacters
↓ eksekusi perintah (docker restart, wsl-deploy, dll)
↓ tulis result-<id>.txt dengan exit code + output
/hermes-queue/ (result)
↓ agent polling baca result
↓ agent melaporkan ke user
Kenapa file ditulis ke temp lalu di-rename? Atomic write. Agent menulis ke file sementara (misal deploy-wordpress-blog.cmd.tmp), lalu rename ke deploy-wordpress-blog.cmd. Watcher hanya membaca file .cmd, bukan .cmd.tmp. Rename adalah operasi atomik di filesystem — watcher tidak akan pernah membaca file setengah jadi.
Format file perintah dan file result
File .cmd berisi satu baris perintah dengan format yang ketat:
# /hermes-queue/deploy-wordpress-blog.cmd
wsl-deploy wordpress blog
File result berisi exit code di baris pertama, diikuti output stdout/stderr:
# /hermes-queue/result-deploy-wordpress-blog.txt
exit=0
Creating db_blog...
User db_blog created
Starting container...
SITE 'blog' TERDEPLOY
Kenapa exit code harus ikut ditulis, bukan cuma stdout? Karena banyak program mengeluarkan output ke stdout bahkan saat gagal. docker restart nonexistent-container mengeluarkan pesan error ke stderr tapi mungkin tetap menulis sesuatu ke stdout. Exit code adalah cara deterministic untuk agent menentukan sukses atau gagal. Tanpa exit code, agent harus menebak dari isi stdout — yang rapuh.
Sisi host: watcher dan whitelist
Watcher adalah script PowerShell (hermes-bridge.ps1, 154 baris) yang dijadwalkan lewat Windows Task Scheduler. Setiap run, script ini:
- Scan folder
C:\hermes-ops\queue\untuk file*.cmd - Sortir berdasarkan creation time (FIFO)
- Validasi setiap perintah
- Eksekusi atau tolak
- Hapus file
.cmdsetelah diproses
Berikut bagian kunci dari watcher — validasi shell metacharacters dan dispatch ke whitelist:
# hermes-bridge.ps1 — validasi + dispatch (versi ringkas)
$QueueDir = "C:\hermes-ops\queue"
$Containers = @("hermes", "n8n", "cloudflared", "mariadb", "site-server", "netdata")
$ProtectedContainers = @("hermes", "cloudflared")
# ... watcher loop ...
$cmd = $raw.Trim()
# REJECT shell metacharacters (security hardening)
if ($cmd -match '[\|\&]{1,2}|[<>]|;\s|`|\$\(|cmd\s*/c|powershell\s') {
Write-Log "REJECTED: shell metacharacters in command: $cmd" "WARN"
Remove-Item $f.FullName -Force
continue
}
$parts = $cmd -split '\s+'
$keyword = $parts[0].ToLower()
switch ($keyword) {
"docker-restart" {
$target = $parts[1]
if ($Containers -contains $target) {
$out = & docker restart $target 2>&1
Write-Log "RESULT: exit=$LASTEXITCODE out=$out"
} else {
Write-Log "REJECTED: container '$target' not whitelisted" "WARN"
}
}
"docker-stop" {
$target = $parts[1]
if ($ProtectedContainers -contains $target) {
Write-Log "REJECTED: PROTECTED container '$target' cannot be stopped" "WARN"
} elseif ($Containers -contains $target) {
$out = & docker stop $target 2>&1
Write-Log "RESULT: exit=$LASTEXITCODE out=$out"
}
}
"wsl-deploy" {
$action = $parts[1]
$name = $parts[2]
if ($action -in @('wordpress','laravel','nextjs','down','list') -and $name -match '^[a-z0-9-]{1,32}$') {
$out = & wsl.exe -e bash -lc "cd ~/ai-stack && ./scripts/deploy.sh $action $name" 2>&1
# Tulis result file
Set-Content -Path "result-$($f.BaseName).txt" -Value "exit=$LASTEXITCODE`r`n$out"
} else {
Write-Log "REJECTED: invalid wsl-deploy action/name" "WARN"
}
}
default {
Write-Log "REJECTED: unknown keyword '$keyword'" "WARN"
}
}
Remove-Item $f.FullName -Force
Whelist lengkap keyword yang diizinkan:
docker-restart <container>— restart container dari daftar yang diizinkandocker-start <container>— start containerdocker-stop <container>— stop container (dikecualikan: hermes, cloudflared)docker-compose <action> <service>— compose up/down/restart/pullwsl-start— start WSL instancewsl-deploy <type> <name>— jalankan deploy scriptdocker-desktop-restart— restart Docker Desktopshutdown-abort— batalkan shutdown terjadwalshutdown-reboot— reboot host (60 detik delay)
Whitelist berbasis daftar-izin, BUKAN daftar-larang. Artinya: apapun yang tidak ada di daftar di atas, ditolak. Bukan "izinkan semua kecuali X". Blacklist selalu bisa ditembus — cukup temukan perintah yang tidak ada di daftar larangan. Allowlist tidak bisa ditembus dengan cara yang sama.
Container yang dilindungi (hermes, cloudflared) di-hard-block dari docker-stop dan docker-compose down. Kenapa? Karena hermes adalah agent sendiri — kalau dihentikan, tidak ada yang bisa memulai kembali dari remote. Dan cloudflared adalah satu-satunya jalur akses publik — kalau dihentikan, semua situs dan akses remote putus.
Sisi agent: mengirim perintah dan menunggu hasil
Agent menulis file .cmd ke folder queue dan polling untuk result:
# Tulis perintah ke queue
echo "docker-restart n8n" > /hermes-queue/restart-n8n.cmd
# Polling untuk result (maks ~90 detik)
RESULT_FILE="/hermes-queue/result-restart-n8n.txt"
for i in $(seq 1 9); do
if [ -f "$RESULT_FILE" ]; then
cat "$RESULT_FILE"
rm -f "$RESULT_FILE"
break
fi
sleep 10
done
# Kalau result tidak muncul dalam 90 detik → timeout
if [ ! -f "$RESULT_FILE" ]; then
echo "ERROR: bridge tidak merespons dalam 90 detik"
fi
Penanganan kalau file result tidak pernah muncul: timeout. Mungkin bridge poller tidak jalan (Task Scheduler mati), atau perintah hang. Agent melaporkan timeout ke user dan tidak mencoba ulang otomatis — ini keputusan disain, bukan bug. Auto-retry untuk perintah yang mungkin sudah dieksekusi (tapi result belum tertulis) bisa menyebabkan double-execution.
Kegagalan yang harus diantisipasi
Race condition: dua file .cmd untuk container yang sama. Agent menulis docker-restart n8n, lalu segera docker-stop n8n. Bridge memproses berurutan (FIFO by creation time), jadi restart dulu, lalu stop. Hasil akhir: container stopped. Tapi kalau agent bermaksud "restart, tunggu, lalu stop untuk maintenance", urutan ini benar. Kalau agent bermaksud "stop segera", race condition bisa menyebabkan delay 60 detik.
File result yatim. Bridge crash setelah eksekusi tapi sebelum menulis result. Agent polling sampai timeout, tidak tahu apakah perintah sudah jalan atau belum. Mitigasi: agent bisa cek state aktual (docker ps) setelah timeout untuk verifikasi.
Perintah hang. docker compose up untuk container dengan build step yang tidak selesai-selesai. Bridge block di baris eksekusi, tidak memproses file .cmd lain di queue. Mitigasi: deploy command (wsl-deploy) punya timeout internal di script deploy. Tapi docker-restart bisa hang kalau container tidak respond.
Queue menumpuk. Kalau agent menulis 10 file .cmd sebelum bridge polling, semua diproses berurutan. Tidak ada parallelisme. Untuk 10 perintah @ 5 detik eksekusi = 50 detik minimum. Mitigasi: agent tidak spam queue. Satu request = satu file .cmd.
Permission mount WSL2. Folder /hermes-queue/ di container harus map ke C:\hermes-ops\queue\ di Windows dengan write permission. Kalau Docker volume mount rusak atau permission berubah, agent bisa menulis file tapi bridge tidak bisa membacanya (atau sebaliknya). Gejala: file .cmd menumpuk di queue tanpa result. Diagnosa: cek ls -la /hermes-queue/ dan bandingkan dengan dir C:\hermes-ops\queue\ di Windows.
Batasan pola ini
Latency polling. Default 60 detik antara poll. Operasi yang harus real-time (alerting, monitoring) tidak cocok. Hanya cocok untuk operasi infra yang toleran terhadap delay.
Tidak cocok untuk perintah interaktif. Perintah yang butuh input user (misal mysql interactive prompt) tidak bisa. Setiap perintah harus selesai tanpa interaksi.
Tidak ada streaming output. Agent tidak bisa melihat output real-time. Hanya mendapat hasil final setelah perintah selesai. Untuk deploy yang butuh 2 menit, agent tidak tahu progress sampai result file muncul.
Satu perintah per file. Tidak bisa menggabungkan multiple command dengan ; atau && (ditolak oleh metacharacter filter). Setiap file .cmd = satu keyword + argumen.
Langkah berikutnya
Pola bridge ini adalah tulang punggung bagaimana agent berkomunikasi dengan host. Bagaimana agent tahu kapan harus pakai jalur ini — bukan mencoba menjalankan docker langsung — adalah topik skill engineering dengan SKILL.md. Untuk gambaran besar sistem lengkap, lihat AI agent yang deploy website otomatis.
FAQ
Apakah bridge ini bisa digunakan untuk perintah Windows selain Docker?
Bisa, tapi harus ditambahkan ke whitelist di hermes-bridge.ps1. Setiap keyword baru butuh: validasi argumen, log entry, dan dispatch logic. Saya sengaja menjaga whitelist pendek — setiap penambahan adalah keputusan keamanan.
Bagaimana kalau seseorang mendapat akses ke folder queue?
Mereka bisa menjalankan perintah yang ada di whitelist (docker-restart, wsl-deploy, dll). Tapi mereka tidak bisa menjalankan perintah arbitrary — metacharacter filter menolak pipes, redirects, dan command chaining. Folder queue harus dibatasi ke proses yang benar (agent + bridge).
Kenapa polling 60 detik, bukan event-based?
Windows Task Scheduler dengan trigger "every 1 minute" adalah cara paling reliable untuk scheduled polling tanpa service permanent. Event-based (filesystem watcher di PowerShell) bisa miss events kalau PowerShell crash. Polling sederhana dan predictable.
Apakah pola ini bisa dipakai di Linux host?
Ya, dengan adjustment: ganti PowerShell watcher dengan bash/inotifywait, ganti Task Scheduler dengan systemd timer atau cron. Konsepnya sama: folder shared + watcher dengan whitelist.
Kesimpulan
Drop-file queue bridge menyelesaikan masalah spesifik: AI agent di container perlu menjalankan perintah terbatas di host Windows, tanpa SSH, tanpa exposure penuh. Trade-off utama adalah latency 60 detik dan tidak ada streaming — keduanya acceptable untuk operasi infra. Bagian tersulit bukan teknisinya, tapi keputusan keamanan: apa yang di-whitelist, apa yang di-block, dan bagaimana audit trail dijaga.
Kalau mau lanjut, baca skill engineering dengan SKILL.md untuk memahami bagaimana agent diajari kapan dan bagaimana menggunakan jalur bridge ini.