Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d2e5053
feat(terminal): add Sprites cloud sandbox backend
kylemclaren May 21, 2026
4954e36
docs(terminal): document Sprites backend; drop region + compute knobs
kylemclaren May 21, 2026
be5ba84
docs(terminal): align Sprites language with sprites.dev positioning
kylemclaren May 21, 2026
0190403
docs(terminal): drop "Firecracker" from Sprites language, prefer "clo…
kylemclaren May 21, 2026
2f6a349
docs(terminal): add Sprites OPTION 7 to cli-config.yaml.example
kylemclaren May 21, 2026
4df877e
feat(terminal): drop SPRITES_BASE_URL override; endpoint is fixed
kylemclaren May 22, 2026
59134fa
docs(terminal): clarify Sprites intentionally skips sync_back
kylemclaren May 22, 2026
6f67d9e
docs(terminal): document Sprites restricted tokens (prefix scoping)
kylemclaren May 22, 2026
5ebb6c0
fix(tools): bound sprites-py to <0.2 per supply-chain pinning policy
kylemclaren May 22, 2026
479911b
test(terminal): add Sprites unit + integration tests
kylemclaren May 22, 2026
4253a46
docs(readme): add Sprites to the terminal-backends summary
kylemclaren May 22, 2026
76ec577
docs(readme): drop the Sprites link from the backends summary
kylemclaren May 22, 2026
b0f2009
docs(readme): group Sprites with Daytona/Modal under serverless persi…
kylemclaren May 22, 2026
62ddef9
feat(terminal): wire Sprites into status/doctor/config CLI surfaces
kylemclaren May 22, 2026
9ca8391
fix(security): register Sprites in approval/blocklist/dispatch sets
kylemclaren May 22, 2026
fb81807
docs(terminal): add Sprites to features/tools.md and security.md
kylemclaren May 22, 2026
fa9f788
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 22, 2026
387b22a
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 23, 2026
d17e228
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 24, 2026
10428c2
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 25, 2026
d99cb8f
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 25, 2026
1a1c457
Merge branch 'NousResearch:main' into add-sprites-terminal-backend
kylemclaren May 26, 2026
7abcf55
Merge branch 'main' into add-sprites-terminal-backend
Jul 13, 2026
4c4b387
fix(terminal): profile-scope Sprite identity + register sprites in cu…
Jul 13, 2026
6882c22
test(terminal): pin sprites dispatch wiring; make _sprite_name a test…
Jul 13, 2026
dfdae48
Merge remote-tracking branch 'upstream/main' into add-sprites-termina…
Aug 18, 2026
dd39668
fix(terminal): register sprites in backend classification surface add…
Aug 18, 2026
a47d3e3
chore(deps): modernize sprites-py pin to >=0.5.0,<0.6
Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ hermes-agent/
β”œβ”€β”€ agent/ # Agent internals (provider adapters, memory, caching, compression, etc.)
β”œβ”€β”€ hermes_cli/ # CLI subcommands, setup wizard, plugins loader, skin engine
β”œβ”€β”€ tools/ # Tool implementations β€” auto-discovered via tools/registry.py
β”‚ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, singularity)
β”‚ └── environments/ # Terminal backends (local, docker, ssh, modal, daytona, vercel_sandbox, sprites, singularity)
β”œβ”€β”€ gateway/ # Messaging gateway β€” run.py + session.py + platforms/
β”‚ β”œβ”€β”€ platforms/ # Adapter per platform (telegram, discord, slack, whatsapp,
β”‚ β”‚ # homeassistant, signal, matrix, mattermost, email, sms,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Use any model you want β€” [Nous Portal](https://portal.nousresearch.com), OpenR
<tr><td><b>A closed learning loop</b></td><td>Agent-curated memory with periodic nudges. Autonomous skill creation after complex tasks. Skills self-improve during use. FTS5 session search with LLM summarization for cross-session recall. <a href="https://github.com/plastic-labs/honcho">Honcho</a> dialectic user modeling. Compatible with the <a href="https://agentskills.io">agentskills.io</a> open standard.</td></tr>
<tr><td><b>Scheduled automations</b></td><td>Built-in cron scheduler with delivery to any platform. Daily reports, nightly backups, weekly audits β€” all in natural language, running unattended.</td></tr>
<tr><td><b>Delegates and parallelizes</b></td><td>Spawn isolated subagents for parallel workstreams. Write Python scripts that call tools via RPC, collapsing multi-step pipelines into zero-context-cost turns.</td></tr>
<tr><td><b>Runs anywhere, not just your laptop</b></td><td>Seven terminal backends β€” local, Docker, SSH, Singularity, Modal, Daytona, and Vercel Sandbox. Daytona and Modal offer serverless persistence β€” your agent's environment hibernates when idle and wakes on demand, costing nearly nothing between sessions. Run it on a $5 VPS or a GPU cluster.</td></tr>
<tr><td><b>Runs anywhere, not just your laptop</b></td><td>Eight terminal backends β€” local, Docker, SSH, Singularity, Modal, Daytona, Vercel Sandbox, and Sprites. Daytona, Modal, and Sprites offer serverless persistence β€” your agent's environment hibernates when idle and wakes on demand, costing nearly nothing between sessions. Run it on a $5 VPS or a GPU cluster.</td></tr>
<tr><td><b>Research-ready</b></td><td>Batch trajectory generation, trajectory compression for training the next generation of tool-calling models.</td></tr>
</table>

Expand Down
7 changes: 4 additions & 3 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ def hud_surface_note(valid_tool_names: "set[str] | None" = None) -> str:
# misleading β€” the agent should only see the machine it can actually touch.
_REMOTE_TERMINAL_BACKENDS = frozenset({
"docker", "singularity", "modal", "daytona", "ssh",
"vercel_sandbox", "managed_modal",
"vercel_sandbox", "managed_modal", "sprites",
})


Expand All @@ -1137,6 +1137,7 @@ def hud_surface_note(valid_tool_names: "set[str] | None" = None) -> str:
"managed_modal": "a managed Modal sandbox (Linux)",
"daytona": "a Daytona workspace (Linux)",
"vercel_sandbox": "a Vercel sandbox (Linux)",
"sprites": "a Sprite β€” a stateful cloud sandbox on Fly.io (Linux)",
"ssh": "a remote host reached over SSH (likely Linux)",
}

Expand Down Expand Up @@ -1244,7 +1245,7 @@ def _probe_remote_backend(env_type: str) -> str | None:
}

container_config = None
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}:
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox", "sprites"}:
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
Expand Down Expand Up @@ -1335,7 +1336,7 @@ def build_environment_hints() -> str:
and a Windows-only note that `terminal` shells out to bash, not
PowerShell).
- For **remote / sandbox** terminal backends (docker, singularity,
modal, daytona, ssh, vercel_sandbox): host info is **suppressed**
modal, daytona, ssh, vercel_sandbox, sprites): host info is **suppressed**
because the agent's tools can't touch the host β€” only the backend
matters. A live probe inside the backend reports its OS, user, $HOME,
and cwd. Falls back to a static summary if the probe fails.
Expand Down
20 changes: 19 additions & 1 deletion cli-config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,26 @@ terminal:
# daytona_image: "nikolaik/python-nodejs:python3.11-nodejs20"
# container_disk: 10240 # Daytona max is 10GB per sandbox

# -----------------------------------------------------------------------------
# OPTION 7: Sprites cloud execution
# Commands run in a Sprite β€” a stateful cloud sandbox on Fly.io with native
# checkpoint & restore. Persistent by default; each Sprite is keyed by task_id
# (hermes-{task_id}) so sessions resume cleanly across Hermes restarts.
# Great for: Persistent cloud sandboxes, agent loops that need state to survive
# Requires: pip install 'hermes-agent[sprites]', SPRITES_TOKEN env var
# Note: Sprites allocates compute dynamically (up to 8 CPU / 16 GB RAM).
# container_cpu / container_memory / container_disk are ignored on
# this backend; no region selector yet.
# -----------------------------------------------------------------------------
# terminal:
# backend: "sprites"
# cwd: "/home/sprite" # Sprite default home; "/root" / "~" auto-rewrite
# timeout: 180
# lifetime_seconds: 300
# container_persistent: true # Leave Sprite alive on cleanup (delete if false)

#
# --- Container resource limits (docker, singularity, modal, daytona -- ignored for local/ssh) ---
# --- Container resource limits (docker, singularity, modal, daytona -- ignored for local/ssh/sprites) ---
# These settings apply to all container backends. They control the resources
# allocated to the sandbox and whether its filesystem persists across sessions.
container_cpu: 1 # CPU cores
Expand Down
3 changes: 3 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4623,6 +4623,9 @@ def show_config():
elif terminal.get('backend') == 'vercel_sandbox':
print(f" Vercel runtime: {terminal.get('vercel_runtime', 'node24')}")
print(f" Vercel auth: {'configured' if get_env_value('VERCEL_OIDC_TOKEN') or (get_env_value('VERCEL_TOKEN') and get_env_value('VERCEL_PROJECT_ID') and get_env_value('VERCEL_TEAM_ID')) else '(not set)'}")
elif terminal.get('backend') == 'sprites':
sprites_token = get_env_value('SPRITES_TOKEN') or get_env_value('SPRITE_TOKEN')
print(f" Sprites token: {'configured' if sprites_token else '(not set)'}")
elif terminal.get('backend') == 'ssh':
ssh_host = get_env_value('TERMINAL_SSH_HOST')
ssh_user = get_env_value('TERMINAL_SSH_USER')
Expand Down
29 changes: 29 additions & 0 deletions hermes_cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,35 @@ def run_doctor(args):
issues,
)

# Sprites (if using sprites backend)
if terminal_env == "sprites":
sprites_token = os.getenv("SPRITES_TOKEN") or os.getenv("SPRITE_TOKEN")
if sprites_token:
check_ok("Sprites token", "(configured)")
else:
_fail_and_issue(
"SPRITES_TOKEN not set",
"(required for TERMINAL_ENV=sprites)",
"Run `sprite login` (or `sprite auth setup --token …`) and put the token in ~/.hermes/.env as SPRITES_TOKEN",
issues,
)

if importlib.util.find_spec("sprites") is not None:
check_ok("sprites-py SDK", "(installed)")
else:
_fail_and_issue(
"sprites-py SDK not installed",
"(pip install 'hermes-agent[sprites]')",
"Install the Sprites optional dependency: pip install 'hermes-agent[sprites]'",
issues,
)

persistent = os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in {"1", "true", "yes", "on"}
if persistent:
check_info("Sprites persistence: Sprite stays alive across sessions; its ext4 filesystem is the authoritative store")
else:
check_info("Sprites persistence: Sprite is deleted on cleanup (ephemeral)")

# Vercel Sandbox (if using vercel_sandbox backend)
if terminal_env == "vercel_sandbox":
runtime = os.getenv("TERMINAL_VERCEL_RUNTIME", "node24").strip() or "node24"
Expand Down
69 changes: 66 additions & 3 deletions hermes_cli/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1340,11 +1340,12 @@ def setup_terminal_backend(config: dict):
"SSH - run on a remote machine",
"Daytona - persistent cloud development environment",
"Vercel Sandbox - cloud microVM with snapshot filesystem persistence",
"Sprites - stateful cloud sandbox on Fly.io, with checkpoint & restore",
]
idx_to_backend = {0: "local", 1: "docker", 2: "modal", 3: "ssh", 4: "daytona", 5: "vercel_sandbox"}
backend_to_idx = {"local": 0, "docker": 1, "modal": 2, "ssh": 3, "daytona": 4, "vercel_sandbox": 5}
idx_to_backend = {0: "local", 1: "docker", 2: "modal", 3: "ssh", 4: "daytona", 5: "vercel_sandbox", 6: "sprites"}
backend_to_idx = {"local": 0, "docker": 1, "modal": 2, "ssh": 3, "daytona": 4, "vercel_sandbox": 5, "sprites": 6}

next_idx = 6
next_idx = 7
if is_linux:
terminal_choices.append("Singularity/Apptainer - HPC-friendly container")
idx_to_backend[next_idx] = "singularity"
Expand Down Expand Up @@ -1592,6 +1593,68 @@ def setup_terminal_backend(config: dict):

_prompt_vercel_sandbox_settings(config)

elif selected_backend == "sprites":
print_success("Terminal backend: Sprites")
print_info("Stateful cloud sandboxes on Fly.io, with checkpoint & restore.")
print_info("Sprites persist between sessions and are reused by task_id.")
print_info("Sign up at: https://sprites.dev")

try:
__import__("sprites")
except ImportError:
print_info("Installing sprites-py SDK...")
import subprocess

# Managed uv first β€” same rationale as the Vercel branch above.
from hermes_cli.managed_uv import ensure_uv

uv_bin = ensure_uv()
if uv_bin:
result = subprocess.run(
[uv_bin, "pip", "install", "--python", sys.executable, "sprites-py"],
capture_output=True,
text=True,
)
else:
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "sprites-py"],
capture_output=True,
text=True,
)
if result.returncode == 0:
print_success("sprites-py installed")
else:
print_warning("Install failed β€” run manually: pip install 'hermes-agent[sprites]'")
if result.stderr:
print_info(f" Error: {result.stderr.strip().splitlines()[-1]}")

# Sprites API token
print()
existing_token = get_env_value("SPRITES_TOKEN")
if existing_token:
print_info(" Sprites token: already configured")
if prompt_yes_no(" Update token?", False):
token = prompt(" Sprites token", password=True)
if token:
save_env_value("SPRITES_TOKEN", token)
print_success(" Updated")
else:
print_info(" Get a token with: sprite login (or `sprite auth setup --token ...`)")
print_info(" Tip: in the dashboard you can mint a Restricted Token with prefix=hermes")
print_info(" to scope it to hermes-* sprites only. Recommended for CI / shared use.")
token = prompt(" Sprites token", password=True)
if token:
save_env_value("SPRITES_TOKEN", token)
print_success(" Configured")

# Drop any previously-saved override of the base URL β€” it's now fixed.
if get_env_value("SPRITES_BASE_URL"):
remove_env_value("SPRITES_BASE_URL")

print()
print_info("Note: Sprites allocates compute dynamically (up to 8 CPU / 16 GB RAM).")
print_info("Manual CPU/memory/disk/region knobs are not yet exposed.")

elif selected_backend == "ssh":
print_success("Terminal backend: SSH")
print_info("Run commands on a remote machine via SSH.")
Expand Down
7 changes: 7 additions & 0 deletions hermes_cli/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,13 @@ def _resolve_env(env_ref) -> str:
print(f" Auth detail: {line}")
print(f" Persistence: {'snapshot filesystem' if persist_enabled else 'ephemeral filesystem'}")
print(" Processes: live processes do not survive cleanup, snapshots, or sandbox recreation")
elif terminal_env == "sprites":
sdk_ok = importlib.util.find_spec("sprites") is not None
sdk_label = "installed" if sdk_ok else "missing (install: pip install 'hermes-agent[sprites]')"
token_ok = bool(os.getenv("SPRITES_TOKEN") or os.getenv("SPRITE_TOKEN"))
token_label = "configured" if token_ok else "(not set β€” required)"
print(f" SDK: {check_mark(sdk_ok)} {sdk_label}")
print(f" Token: {check_mark(token_ok)} {token_label}")

sudo_password = os.getenv("SUDO_PASSWORD", "")
print(f" Sudo: {check_mark(bool(sudo_password))} {'enabled' if sudo_password else 'disabled'}")
Expand Down
2 changes: 1 addition & 1 deletion hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ def _timezone_options() -> List[str]:
"terminal.backend": {
"type": "select",
"description": "Terminal execution backend",
"options": ["local", "docker", "ssh", "modal", "daytona", "vercel_sandbox", "singularity"],
"options": ["local", "docker", "ssh", "modal", "daytona", "vercel_sandbox", "sprites", "singularity"],
},
"terminal.vercel_runtime": {
"type": "select",
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ edge-tts = ["edge-tts==7.2.7"]
modal = ["modal==1.3.4"]
daytona = ["daytona==0.155.0"]
vercel = ["vercel==0.7.2"]
sprites = ["sprites-py>=0.5.0,<0.6"]
hindsight = ["hindsight-client==0.6.1"]
dev = ["debugpy==1.8.20", "pytest==9.1.1", "pytest-asyncio==1.3.0", "mcp==2.0.0", "httpx2==2.7.0", "starlette==1.3.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==83.0.0"] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83)
messaging = ["python-telegram-bot[webhooks]==22.8", "discord.py[voice]==2.7.1", "aiohttp==3.14.3", "brotlicffi==1.2.0.1", "slack-bolt==1.30.0", "slack-sdk==3.43.0", "qrcode==7.4.2"] # aiohttp 3.14.3: prior CVEs + GHSA-cq5v-8q36-5273/GHSA-mfx4-hv73-q22v/GHSA-mq44-7p77-q5h7
Expand Down Expand Up @@ -344,7 +345,7 @@ all = [
#
# Removed from [all] on 2026-05-12 (covered by lazy-install):
# anthropic, exa, firecrawl, parallel-web, fal, edge-tts,
# modal, daytona, vercel, messaging (telegram/discord/slack),
# modal, daytona, vercel, sprites, messaging (telegram/discord/slack),
# matrix, slack, honcho, voice (faster-whisper),
# dingtalk, feishu, bedrock, tts-premium (elevenlabs)
#
Expand Down
2 changes: 1 addition & 1 deletion tests/agent/test_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ def test_environment_hint_from_env_var_is_appended(self, monkeypatch):
def test_remote_backend_list_covers_known_sandboxes(self):
"""Regression guard: if someone adds a remote backend, they must list it here."""
import agent.prompt_builder as _pb
for backend in ("docker", "singularity", "modal", "daytona", "ssh", "vercel_sandbox"):
for backend in ("docker", "singularity", "modal", "daytona", "ssh", "vercel_sandbox", "sprites"):
assert backend in _pb._REMOTE_TERMINAL_BACKENDS, (
f"{backend!r} must be in _REMOTE_TERMINAL_BACKENDS so its host "
f"info is suppressed in the system prompt"
Expand Down
Loading