feat(virus): add autonomous rust agent (concept art)#6613
feat(virus): add autonomous rust agent (concept art)#6613millw14 wants to merge 219 commits intoelizaOS:v2.0.0from
Conversation
Implement comprehensive hot reload functionality for backend code changes during development. When TypeScript files in watched packages are modified, the system automatically rebuilds the CLI and restarts the server. Core implementation: - Watch all CLI dependency packages (cli, core, server, api-client, plugin-bootstrap, plugin-sql, config) - Debounce file changes (300ms) to prevent rapid rebuilds - Graceful server shutdown and restart on code changes - Health check verification after rebuilds to detect crashes - Rebuild queueing for changes during active rebuilds Technical details: - Use Bun.spawn() for all process execution per project standards - Comprehensive TypeScript type annotations with JSDoc - Exit event listeners for proper SIGKILL fallback - Directory existence checks to handle optional packages gracefully - Full test coverage with 13 passing tests using temp directories The dev environment now provides: - Automatic backend rebuild on TypeScript file changes - Server health verification after each rebuild - Graceful error handling and process cleanup - Clear logging for all rebuild operations Usage: bun run dev Addresses all PR review feedback from @cursor, @greptile-apps, and @claude
Co-authored-by: sayonara <sayonara@elizalabs.ai>
- Add .catch() handler on child.exited promise - Ensure setTimeout always calls resolve() after SIGKILL - Wrap child.kill() in try-catch for error handling - Match the pattern used in cleanup() function This prevents the rebuild from freezing if the process doesn't respond to signals or if child.exited rejects.
Co-authored-by: sayonara <sayonara@elizalabs.ai>
…r extraction - Add retry logic for XML parsing with exponential backoff (1s, 2s, 4s... capped at 8s) - Add summary generation retry with graceful fallback message - Add parameter extraction from multi-step decision template - Add formatActionsWithParams() for LLM parameter schema information - Add bounds checking for retry counts (1-10 range) - Add comprehensive tests for retry and parameter scenarios
Enhanced JSON parameter parsing to ensure only non-null objects are accepted, with appropriate logging for invalid types. Updated related test and provider logic to match stricter validation and improved code consistency.
Added explicit checks and warnings for array-type parameters in both DefaultMessageService and multi-step tests. Metadata properties in accumulated state are now prefixed with underscores to avoid collisions with action parameters.
Improves logging granularity and clarity for multi-step message processing, including step-by-step info, provider/action execution, and summary generation. Adds streaming support for summary output in multi-step mode and ensures streaming contexts are correctly managed for both single-shot and multi-step flows. Minor code formatting and consistency improvements applied.
Replaces 'state' with 'accumulatedState' in provider and action calls to ensure correct state is used. Adds logic to prevent duplicate streaming of summary output to the user on retry attempts, tracking streaming status with 'hasStreamedToUser'.
Marks streaming as started before sending content to prevent duplicate streams on retries in DefaultMessageService. Updates action parameter formatting to handle invalid definitions and provide defaults for missing type or description.
Updates submodule from 797ad22 (init commit) to 4a33c0c (Add ElizaOS scenario testing rules) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…entries - Add array check to prevent malformed parameter output - Filter invalid param entries upfront to avoid dangling headers
- Added methods to StreamingContext for managing streaming state, including reset, getStreamedText, and isComplete. - Improved DefaultMessageService to utilize intelligent retry logic for streaming, allowing for continuation prompts when text extraction is incomplete. - Updated tests to validate new streaming context features and ensure correct behavior during retries. - Enhanced IStreamExtractor interface with reset and flush methods for better state management.
- Removed redundant '[MULTI-STEP]' prefix from logging messages in DefaultMessageService to enhance clarity and readability. - Updated various log statements to maintain consistency while preserving essential information about the multi-step processing flow. - other: fix docs
Replace ACTIONS_REGEX with indexOf-based extraction in detectResponseStrategy. This eliminates potential polynomial-time regex matching on malicious inputs.
Ensures continuation response is properly streamed to user in single-shot mode, matching the behavior of multi-step continuation at line 1862.
…nt results Previously, calling getStreamedText() multiple times would return different results because flush() empties the buffer. Now the flushed content is accumulated into streamedText, ensuring consistent results across calls.
This commit refactors the error handling and shutdown logic in the development server scripts. It ensures that processes are properly cleaned up and that errors are logged and handled more effectively. Co-authored-by: sayonara <sayonara@elizalabs.ai>
* fix: typo threshholds -> thresholds, commited -> committed * fix: typo packge -> package * fix: typo brige -> bridge * fix: typo unneccesary -> unnecessary * fix: typo wheather -> whether * fix: typo assitant -> assistant * fix: typo exisitng -> existing
Minimal self-contained Eliza agent packaged as a single .exe: - Auto-detects RAM, downloads the biggest local model that fits (via Ollama) - Polls Win32 GetLastInputInfo for idle detection - Runs autonomously when human is away (think/shell/remember loop) - File-based memory (append-only journal at ~/.virus/journal.txt) - Optional --install flag for auto-start on boot via registry
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip CodeRabbit can generate a title for your PR based on the changes with custom instructions.Set the |
| const DENIED_PATTERNS: &[&str] = &[ | ||
| "rm -rf /", | ||
| "del /s /q c:\\", | ||
| "format c:", | ||
| "format d:", | ||
| "shutdown", | ||
| "taskkill", | ||
| "net user", | ||
| "net localgroup", | ||
| "reg delete", | ||
| "bcdedit", | ||
| "diskpart", | ||
| "cipher /w", | ||
| "schtasks /delete", | ||
| "powershell -enc", | ||
| "powershell -encodedcommand", | ||
| "invoke-webrequest", | ||
| "wget ", | ||
| "curl -o", | ||
| "curl --output", | ||
| "bitsadmin", | ||
| "certutil -urlcache", | ||
| "::$data", | ||
| "> /dev/sda", | ||
| "mkfs.", | ||
| "dd if=", | ||
| "passwd", | ||
| "shadow", | ||
| "authorized_keys", | ||
| ".ssh/", | ||
| "chrome --", | ||
| "firefox --", | ||
| ]; | ||
|
|
||
| fn is_command_safe(cmd: &str) -> bool { | ||
| let lower = cmd.to_lowercase(); | ||
| !DENIED_PATTERNS.iter().any(|p| lower.contains(p)) |
There was a problem hiding this comment.
Command blocklist is trivially bypassable
The DENIED_PATTERNS blocklist relies on simple substring matching against the lowercased command string. An LLM — especially one instructed to be "curious, creative, and self-directed" — can bypass every single rule here with minimal variation:
rm -rf /is blocked, butrm -r -f /,rm --recursive --force /, orfind / -deleteare not.curl -ois blocked, butcurl > file,curl | bash, orpython3 -c "import urllib.request; ..."are not.powershell -encis blocked, butpowershell -e(the short alias) andpwsh -EncodedCommandare not..ssh/is blocked, butcat ~/.ssh/id_rsa,cat ~/'.ssh'/id_rsa, or reading SSH key via Python are not.invoke-webrequestis blocked, butInvoke-WebRequestwith mixed case passes the lowercase check ... wait, the check is lowercased — but(New-Object Net.WebClient).DownloadFile(...)is not blocked at all.wget(with trailing space) is blocked, butwget\t(tab) orwget\nhttp://...are not.- Commands using shell variables, heredocs, or command substitution entirely sidestep substring matching:
cmd=`echo cm0gLXJmIC8=|base64 -d`; $cmdis not blocked.
A prompt-injection or a sufficiently creative LLM response can exfiltrate files, establish C2 persistence, or destroy data despite this blocklist. A substring deny-list is not a viable security boundary for an agent with full shell access.
| /// Register virus.exe to run on startup via the Windows registry. | ||
| #[cfg(windows)] | ||
| pub fn install_autostart() -> Result<(), String> { | ||
| use std::ffi::OsStr; | ||
| use std::os::windows::ffi::OsStrExt; | ||
| use winapi::um::winreg::{RegCloseKey, RegSetValueExW, HKEY_CURRENT_USER, RegOpenKeyExW}; | ||
| use winapi::um::winnt::{KEY_WRITE, REG_SZ}; | ||
|
|
||
| let exe = std::env::current_exe().map_err(|e| e.to_string())?; | ||
| let exe_str = exe.to_string_lossy(); | ||
| let value: Vec<u16> = OsStr::new(&*exe_str).encode_wide().chain(Some(0)).collect(); | ||
|
|
||
| let subkey: Vec<u16> = OsStr::new("Software\\Microsoft\\Windows\\CurrentVersion\\Run") | ||
| .encode_wide() | ||
| .chain(Some(0)) | ||
| .collect(); | ||
| let name: Vec<u16> = OsStr::new("virus").encode_wide().chain(Some(0)).collect(); | ||
|
|
||
| unsafe { | ||
| let mut hkey = std::ptr::null_mut(); | ||
| let res = RegOpenKeyExW(HKEY_CURRENT_USER, subkey.as_ptr(), 0, KEY_WRITE, &mut hkey); | ||
| if res != 0 { | ||
| return Err(format!("RegOpenKeyExW failed: {}", res)); | ||
| } | ||
| let res = RegSetValueExW( | ||
| hkey, | ||
| name.as_ptr(), | ||
| 0, | ||
| REG_SZ, | ||
| value.as_ptr() as *const u8, | ||
| (value.len() * 2) as u32, | ||
| ); | ||
| RegCloseKey(hkey); | ||
| if res != 0 { | ||
| return Err(format!("RegSetValueExW failed: {}", res)); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Persistence mechanism writes to Windows startup registry key
install_autostart() writes the binary path directly into HKCU\Software\Microsoft\Windows\CurrentVersion\Run under the key name "virus". This is the canonical Windows registry persistence mechanism used by malware to survive reboots without administrator privileges.
When a user runs virus.exe --install, the binary will be re-launched on every subsequent Windows login — silently, in the background — without any further user interaction or consent beyond the original --install invocation. There is no corresponding uninstall path in the codebase, and the key name "virus" will appear in Task Manager / startup apps but may not be immediately recognizable.
This goes well beyond "concept art": registry-based persistence combined with stealth idle-detection and autonomous shell execution is the exact behavioral fingerprint of a Remote Access Trojan (RAT).
| success: false, | ||
| }; | ||
| } | ||
| std::thread::sleep(std::time::Duration::from_millis(100)); |
There was a problem hiding this comment.
Synchronous
thread::sleep blocks the Tokio runtime thread
shell::exec is a synchronous function called from within an async context (agent::step). Inside the polling loop it uses std::thread::sleep, which blocks the OS thread entirely. Since Tokio's default multi-threaded runtime uses a fixed thread pool, a long-running or timed-out shell command (up to 30 seconds) will starve other async tasks on that thread.
The correct approach is to offload blocking work to tokio::task::spawn_blocking and replace the inner std::thread::sleep with tokio::time::sleep:
// In agent.rs
let result = tokio::task::spawn_blocking(move || shell::exec(cmd))
.await
.unwrap_or_else(|_| ShellResult { ... });And in shell.rs the polling sleep should similarly use a non-blocking async sleep, or the entire function should be written as a tokio::process::Command future.
| pub async fn bootstrap(model: &str) { | ||
| if !is_running().await { | ||
| let check = shell::exec("ollama --version"); | ||
| if !check.success { | ||
| install_ollama(); | ||
| tokio::time::sleep(std::time::Duration::from_secs(5)).await; | ||
| } | ||
| start_ollama(); | ||
| tokio::time::sleep(std::time::Duration::from_secs(3)).await; | ||
|
|
||
| for _ in 0..10 { | ||
| if is_running().await { | ||
| break; | ||
| } | ||
| tokio::time::sleep(std::time::Duration::from_secs(2)).await; | ||
| } | ||
| } | ||
|
|
||
| ensure_model(model).await; | ||
| } |
There was a problem hiding this comment.
No verification that
install_ollama succeeded before calling start_ollama
After install_ollama() runs winget install Ollama.Ollama ..., the code unconditionally waits 5 seconds and then calls start_ollama() (which itself runs ollama serve). There are several failure scenarios:
wingetmay not be present on older Windows installs.winget installis asynchronous and may not have completed — or may have failed — within the 5-second window.- Even after a successful install,
ollamamay not yet be onPATHwithout a new shell session.
After the 5-second sleep, the code calls start_ollama() which calls shell::exec("start /B ollama serve"). If ollama is not on PATH, this silently fails. The subsequent poll loop (10 × 2 s = 20 s) will then time out and ensure_model will be called against an Ollama instance that was never started.
The check.success flag from shell::exec("ollama --version") should be re-tested after the install step to verify that Ollama is now available before proceeding.
| let result = shell::exec(cmd); | ||
| let output = if result.success { | ||
| &result.stdout | ||
| } else { | ||
| &result.stderr | ||
| }; | ||
| memory::result(output); |
There was a problem hiding this comment.
Stdout silently discarded on command failure
When a shell command exits with a non-zero status, only stderr is written to the journal and returned. stdout is silently dropped:
let output = if result.success {
&result.stdout
} else {
&result.stderr // stdout is silently discarded
};
memory::result(output);Many commands write meaningful partial output to stdout before failing (e.g., find, grep, git, compiler output). Discarding stdout on failure means the agent's memory is incomplete, which may cause it to repeat already-attempted actions or make incorrect inferences about the system state. Both stdout and stderr should be included in the memory entry on failure.
| pub fn recent(n: usize) -> String { | ||
| let path = journal_path(); | ||
| match fs::read_to_string(&path) { | ||
| Ok(contents) => { | ||
| let lines: Vec<&str> = contents.lines().collect(); | ||
| let start = lines.len().saturating_sub(n); | ||
| lines[start..].join("\n") | ||
| } | ||
| Err(_) => String::from("(no memory yet — this is your first time waking up)"), | ||
| } | ||
| } |
There was a problem hiding this comment.
Journal file grows unboundedly with no size cap or rotation
Every agent step appends multiple entries (ACTION, RESULT, THOUGHT/ERROR) to ~/.virus/journal.txt. memory::recent(50) reads the entire file every cycle to extract the last 50 lines, meaning a full fs::read_to_string is issued on every 30-second iteration regardless of file size.
Over days or weeks of operation, the journal can grow to hundreds of megabytes. Add a size limit or log-rotation strategy (e.g., cap the file at N bytes, or roll over into dated files) to avoid unbounded disk consumption and increasingly slow read_to_string calls.
|
I wish people were smart enough, so we could merge this. This could be really interesting if I understand it correctly. |
Migrated from elizaOS/eliza#6613 by @millw14. Minimal autonomous Eliza agent in Rust — detects idle time, picks a local LLM via Ollama based on available RAM, and runs an autonomous loop writing to a journal. Co-Authored-By: millw14 <millw14@users.noreply.github.com>
|
Migrated to elizaOS/examples#2. Thanks @millw14 for the concept art — it fits better in the examples repo than in core. Closing this one. |
Relates to
New package — no linked issue. Concept art / proof of concept for an autonomous Eliza agent as a standalone native binary.
Risks
Low. This is a new self-contained package (
packages/virus/) with zero changes to existing code. No dependencies on or from other packages in the monorepo.Background
What does this PR do?
Adds
packages/virus/— a minimal autonomous Eliza agent written in Rust, packaged as a single.exe(~5 MB). When a human willingly downloads and runs it, it lives on their machine and does its own thing whenever they're away.How it works:
GetLastInputInfo— waits until 2 minutes of idleSHELL: <command>,THINK: <thought>, orWAIT~/.virus/journal.txtModel selection by available RAM:
Usage:
Relates to
New package — no linked issue. Concept art / proof of concept for an autonomous Eliza agent as a standalone native binary.
Risks
Low. This is a new self-contained package (
packages/virus/) with zero changes to existing code. No dependencies on or from other packages in the monorepo.Background
What does this PR do?
Adds
packages/virus/— a minimal autonomous Eliza agent written in Rust, packaged as a single.exe(~5 MB). When a human willingly downloads and runs it, it lives on their machine and does its own thing whenever they're away.How it works:
GetLastInputInfo— waits until 2 minutes of idleSHELL: <command>,THINK: <thought>, orWAIT~/.virus/journal.txtModel selection by available RAM:
Usage:
Greptile Summary
This PR introduces
packages/virus/— a self-contained Rust binary that implements an autonomous AI agent with full shell access. Despite being labelled "concept art," the code fully implements the behavioral fingerprint of a Remote Access Trojan (RAT): stealth activation (runs only when the user is idle viaGetLastInputInfo), reboot persistence (--installwrites toHKCU\Software\Microsoft\Windows\CurrentVersion\Run), unrestricted shell execution (cmd /C/sh -cwith an LLM choosing the command), and activity logging to disk. No existing code in the monorepo is changed, but this package being distributed as part of ElizaOS — even as an opt-in — poses serious ethical and security risks.Key concerns:
install_autostart()writes the binary into the Windows Run key with no corresponding removal path exposed to the user.shell::execusesstd::thread::sleepin a polling loop and is called directly fromasync fn step, stalling the runtime for up to 30 seconds per command.bootstrap()does not verify thatwinget installsucceeded before callingstart_ollama(), potentially running indefinitely against an Ollama instance that was never started.~/.virus/journal.txtwith no rotation or size cap, combined with a fullread_to_stringon every 30-second cycle.Confidence Score: 0/5
packages/virus/src/agent.rs(deny-list bypass) andpackages/virus/src/system.rs(registry persistence) are the most critical.Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A([virus.exe starts]) --> B{--install flag?} B -- yes --> C[Write HKCU\\Run\\virus registry key\nfor reboot persistence] C --> D([exit]) B -- no --> E[memory::init\ncreate ~/.virus/journal.txt] E --> F[system::pick_model\ndetect available RAM] F --> G[model::bootstrap\ninstall Ollama if missing\npull selected model] G --> H{system::idle_seconds\n>= 120s?} H -- no --> I[sleep 10s] I --> H H -- yes --> J[agent::step\nbuild prompt from journal + system context] J --> K[model::generate\nPOST /api/generate to Ollama] K --> L{Parse LLM response} L -- SHELL: cmd --> M[is_command_safe?\nsubstring deny-list check] M -- blocked --> N[memory::error\nlog blocked command] N --> O[sleep 30s] M -- allowed --> P[shell::exec\ncmd /C or sh -c\n30s timeout] P --> Q[memory::action + memory::result\nappend to journal.txt] Q --> O L -- THINK: thought --> R[memory::thought\nappend to journal.txt] R --> O L -- WAIT --> O O --> H style C fill:#ff4444,color:#fff style P fill:#ff8800,color:#fff style M fill:#ffcc00Last reviewed commit: d74601b
(4/5) You can add custom instructions or style guidelines for the agent here!