feat(tools): add Sprites stateful sandbox backend 👾 - #30112
Conversation
015e4fe to
f8d2b98
Compare
Adds a new TERMINAL_ENV=sprites option backed by the sprites-py SDK
(Fly.io). Persistent by default; sprites are keyed by hermes-{task_id}
so sessions resume cleanly across restarts. Verified end-to-end against
api.sprites.dev (exec, cwd tracking, env persistence, stdin heredoc,
exit codes, file sync, ephemeral vs persistent cleanup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprites does not yet expose region or per-sandbox compute sizing (CPU/memory/disk) to API consumers, so the SpriteConfig and the setup flow are simplified to match: sprite creation no longer passes a SpriteConfig at all, the setup wizard no longer prompts for region or container resources, and the docs YAML example drops the container_cpu/memory/disk knobs with a note that they are ignored on this backend. Adds the per-backend section (mirrors Daytona/Vercel pattern), the SPRITES_TOKEN and SPRITES_BASE_URL env-var rows, and the troubleshooting bullet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per sprites.dev, a Sprite is a hardware-isolated, stateful Firecracker VM on Fly.io with checkpoint & restore — not just a generic "cloud sandbox." This normalizes the wording across the user guide, env-var reference, setup wizard, and module docstrings: - "Sprite" (singular, capitalized) for an instance; "Sprites" for the service/product - Backend description leans on Firecracker / Fly.io / stateful framing instead of the generic "cloud sandbox / cloud VM" labels - Compute-sizing note is reworded to match the platform's dynamic allocation model (up to 8 CPU / 16 GB RAM) rather than implying static defaults Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ud sandbox" Per maintainer preference, the public-facing description shouldn't lean on the underlying hypervisor name. Keeps the "stateful sandbox / checkpoint & restore" framing aligned with sprites.dev but reverts the implementation detail to the generic "cloud sandbox" wording used by Modal/Daytona/Vercel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the Daytona section structure: requirements, what it's good for, the dynamic-compute-allocation caveat, and a minimal YAML stanza. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Sprites API endpoint is static (api.sprites.dev) — there is no self-hosted deployment story to support, so exposing a base-URL override in setup, .env, and docs was just noise. SpritesClient is now constructed with no base_url kwarg (lets the SDK use its own default). Setup keeps a one-line cleanup that removes any previously-saved SPRITES_BASE_URL from existing users' .env on next `hermes setup terminal` run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Sprites backend deliberately doesn't copy agent-modified files back to ~/.hermes/cache/remote-syncs/... on cleanup the way SSH/Modal/Daytona do. Those backends need it because their sandboxes are torn down or reset between sessions; Sprites' ext4 filesystem is persistent and the same Sprite (by task_id) is resumed on the next session with all state intact, so a sync_back would just duplicate the canonical store. - sprites.py cleanup() drops the no-op sync_manager.sync_back() call and replaces it with a comment explaining the design choice. - configuration.md splits "Credential files" into push (still applies) and a new "No sync-back, by design" note; the Remote-to-Host File Sync section calls out the Sprites carve-out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sprites tokens default to full-account access, but the dashboard
(Account → Tokens → ⚙ → Restricted Token Options) can mint tokens
scoped to a name prefix and a max-sprites cap. Pair this with our
deterministic hermes-{task_id} naming by creating a hermes-prefixed
token — the token can manage everything Hermes spawns and nothing else.
- configuration.md: new "Restricted tokens" subsection under Sprites
authentication, explaining the two restriction knobs and why the
hermes prefix is the right default for CI / shared envs.
- setup.py: surface the same tip inline when the wizard prompts for
the token so first-time users see it before pasting an unrestricted
one.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md (post Mar/May 2026 supply-chain rules) requires every new PyPI dependency to declare a `<next_major` ceiling rather than an exact pin. `0.0.1rc37` falls under the pre-1.0 rule: floor must include the rc tag so pip opts in to the pre-release; ceiling is `<0.(current_minor + 2) = <0.2`. Future 0.0.x / 0.1.x patches resolve; a hostile 0.2.0 doesn't. pip dry-run confirms the new spec still resolves to 0.0.1rc37 today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unit tests (tests/tools/test_sprites_environment.py): 18 cases against a mocked sprites-py SDK — no token, no network. Cover construction (missing-token error, persistent get-first, create-when-not-found, no compute kwargs, no base_url kwarg), cwd resolution (default /root → detected home, ~ rewrite, explicit cwd preserved), cleanup (persistent leaves the Sprite alive, ephemeral deletes it, idempotency, client.close), _run_bash exit-code surfacing (zero, ExitError → 7, TimeoutError → 124), filesystem push (write_bytes + parent.mkdir, unlink per path), and the _stdin_mode = heredoc declaration. Integration tests (tests/integration/test_sprites_terminal.py): 8 cases against the live api.sprites.dev — gated by SPRITES_TOKEN and @pytest.mark.integration. Module-level skip when the token is absent. Token is captured at import time and re-injected via an autouse fixture because the project conftest's hermetic env wipes everything ending in _TOKEN. Covers basic exec / non-zero exit / OS info / Python availability, write+read, env var persistence across calls, the sprite-env info identity check (asserts hermes-default substring and that the in-Sprite boot_id differs from the host's), and filesystem persistence across a session recycle. Verified locally via scripts/run_tests.sh — 24,007/24,033 pass (26 pre-existing failures in unrelated test files: acp, gateway systemd, browser binary lookup, etc.). 18 unit tests pass under per-file isolation in ~5 min; integration tests pass against the live API in ~80s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps "Seven" → "Eight" and adds a one-sentence framing for Sprites: stateful Fly.io sandboxes with native checkpoint & restore that resume session-to-session (vs. Modal/Daytona, which hibernate-and-wake). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the styling of every other backend name in the same sentence — none of the others link out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stence Sprites' hibernate-when-idle / wake-on-demand cost model is the same as Daytona's and Modal's, so the single grouped sentence carries it without needing a dedicated callout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Sprites backend was already wired through the agent runtime
(_create_environment, requirements check) but missing from the
diagnostic CLI surfaces, so users with sprites configured got a bare
"Backend: sprites" with no token/SDK detail and `hermes doctor` had no
proactive check.
- hermes_cli/status.py: new branch reporting sprites-py install status
and whether SPRITES_TOKEN is set.
- hermes_cli/doctor.py: dedicated block mirroring the Daytona/Vercel
pattern — checks SPRITES_TOKEN presence, SDK install, and prints the
persistence semantics ("Sprite stays alive" vs "Sprite is deleted on
cleanup").
- hermes_cli/config.py: new branch in `hermes config show` reporting
whether the token is configured.
- AGENTS.md: add sprites (and the previously-missed vercel_sandbox)
to the project-structure backends listing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Several cross-cutting registrations only listed the prior sandboxed backends (docker / singularity / modal / daytona / vercel_sandbox); Sprites is also a remote, hardware-isolated sandbox and needs the same treatment. Without this, the agent path on a Sprites backend hits false dangerous-command approval prompts, leaks SPRITES_TOKEN to local- backend subprocesses, and silently drops container_persistent overrides from the code_execution_tool / file_tools dispatch paths. - tools/approval.py: add "sprites" to both sandboxed-backend skip sets (the agent's command is running inside the Sprite, not on the host — same isolation guarantee as the other cloud backends). - tools/environments/local.py: add SPRITES_TOKEN / SPRITE_TOKEN to the provider env blocklist so they are stripped from local-backend child process environments (matches the VERCEL_*, DAYTONA_API_KEY, and MODAL_TOKEN_* treatment). - tools/skills_tool.py: add "sprites" to _REMOTE_ENV_BACKENDS so the skills tool routes its remote/local distinction correctly. - tools/file_tools.py: add "sprites" to the container_config dispatch set so container_persistent: false can take effect through the file-tool code path. - tools/code_execution_tool.py: same dispatch fix (I had removed it in 015e4fe5b on the grounds that sprites ignores CPU/memory/disk — but container_persistent IS honored). - hermes_cli/web_server.py: add "sprites" to the dashboard's terminal.backend select-control options. Surfaced by comparing this branch against NousResearch#17445 (the Vercel Sandbox backend PR), which had to make every one of these registrations explicitly. Same audit applies here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the placement vercel_sandbox got in PR NousResearch#17445: - features/tools.md: row in the backend comparison table, "sprites" added to the backend-enum comment, and a dedicated "Sprites (Fly.io)" subsection covering install + auth, the hermes-{task_id} resume model, the restricted-token recommendation for CI / shared envs, the persistence semantics, and the "no sync-back, by design" rationale. - security.md: container-bypass info note and production-tip paragraph both mention sprites; comparison table gains a row showing dangerous-command checks are skipped (because the Sprite is the security boundary). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c918b78 to
fb81807
Compare
|
@teknium1 what's a good way to get this over the line? |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the substantial backend implementation and the mocked/live coverage.
Problems
tools/environments/sprites.py:83names persistent resources fromtask_id, but the branch resolver maps ordinary top-level calls and delegated children todefault(tools/terminal_tool.py:973-989). The new integration test explicitly acceptshermes-defaultfor every supplied task id (tests/integration/test_sprites_terminal.py:103-106). That makes durable state shared across normal sessions rather than session-scoped.- This branch predates current backend classification paths. Current main requires remote backends to be represented in
tools/terminal_tool.py:1216,tools/file_tools.py:206-212,agent/prompt_builder.py:891-908,tools/env_probe.py:48-53, andtools/approval.py:2242-2253. The PR does not cover that current surface, so a direct salvage would retain host-path/host-prompt behavior in some paths.
Suggested changes
- Define a stable, profile/session-safe Sprite identity and test both intended resume and isolation between independent sessions.
- Salvage the backend onto current main's shared backend classifications, including file-tool-first and prompt/probe paths, then add tests for those routes.
Automated hermes-sweeper review.
# Conflicts: # README.md # hermes_cli/config.py # hermes_cli/doctor.py # hermes_cli/setup.py # hermes_cli/status.py # hermes_cli/web_server.py # pyproject.toml # tools/approval.py # tools/code_execution_tool.py # tools/environments/__init__.py # tools/environments/local.py # tools/file_operations.py # tools/file_tools.py # tools/lazy_deps.py # tools/skills_tool.py # tools/terminal_tool.py # website/docs/reference/environment-variables.md # website/docs/user-guide/configuration.md # website/docs/user-guide/features/tools.md # website/docs/user-guide/security.md
…rrent backend classifications Addresses the hermes-sweeper salvage review on NousResearch#30112. Problem 1 — durable state shared across sessions: the Sprite name was `hermes-{task_id}`, and the task-id resolver collapses ordinary sessions to `default`, so every session shared one live `hermes-default` Sprite (its processes, sockets, and PID space — not just a filesystem snapshot). Scope the name by the active Hermes profile via `_resolve_sprite_name` (`hermes-{profile}-{task_id}`; unchanged `hermes-{task_id}` on the default profile for backward compatibility) so independent profiles never resume into one another's live Sprite, while the same (profile, task_id) still resumes. Names are slugified to a Fly/DNS-safe form. Problem 2 — branch predated current classification paths: register `sprites` in the shared backend classifications main grew after this branch forked — `_REMOTE_TERMINAL_BACKENDS` + `_BACKEND_FALLBACK_DESCRIPTIONS` (host-info suppression / live probe in the system prompt), `_CONTAINER_BACKENDS` (cwd sanitization), and the container_config builder (so `container_persistent` reaches the backend and ephemeral mode works). Tests: add TestSpriteNaming (resume + cross-profile isolation + slugification + resolver-failure fallback); the integration identity test now derives the expected name instead of hard-coding `hermes-default`; extend the container / prompt set-pinning guards to include sprites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed contract - TestDispatchWiring drives the real terminal_tool() body and asserts the container_config builder includes sprites (container_config=None would silently discard container_persistent: false, making ephemeral mode unreachable), plus pins the _create_environment → SpritesEnvironment kwarg handoff (persistent_filesystem, task_id, cwd). - _sprite_name is now read at runtime (cleanup-failure log) and asserted in the construction tests instead of being a write-only attribute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l-backend # Conflicts: # README.md # agent/prompt_builder.py # hermes_cli/config.py # hermes_cli/doctor.py # hermes_cli/setup.py # hermes_cli/status.py # hermes_cli/web_server.py # pyproject.toml # tests/agent/test_prompt_builder.py # tests/tools/test_container_cwd_sanitize.py # tools/approval.py # tools/code_execution_tool.py # tools/environments/__init__.py # tools/environments/local.py # tools/file_operations.py # tools/file_tools.py # tools/lazy_deps.py # tools/skills_tool.py # tools/terminal_tool.py # website/docs/reference/environment-variables.md # website/docs/user-guide/configuration.md # website/docs/user-guide/features/tools.md # website/docs/user-guide/security.md
…ed since July Upstream grew new shared classification sites while this branch aged; sweep them so sprites keeps remote/container semantics everywhere: - tools/env_probe.py _REMOTE_BACKENDS (host Python-state probe line must not leak into a sprites session's prompt; explicitly kept in sync with prompt_builder._REMOTE_TERMINAL_BACKENDS) - tools/file_tools.py _CONTAINER_PATH_BACKENDS_FALLBACK + class-name sniff in _terminal_env_type_for_task - tools/terminal_tool.py container_backend env-var parse gate - agent/prompt_builder.py _probe_remote_backend container_config set - tools/credential_files.py cache-path translation (sprites homes are ~/.hermes like ssh/daytona/vercel, not host paths) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rc37-era pin predates the SDK's stable series. Full live integration suite (8 e2e tests vs api.sprites.dev) verified against 0.5.0; client, sprite, filesystem, and exception surfaces are all compatible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
This comment is an answer to the automated hermes-sweeper review. It maps each problem to the commits that correct the problem. Problem 1: All usual sessions shared one durable SpriteThe old code made the Sprite name from Commit 4c4b387 corrects this. The function The result is this. Two different profiles cannot attach to one live Sprite. The same profile and the same task always attach to the same Sprite. Note: Sessions in one profile share one Sprite. This is the design, not an error. The Daytona backend on main has the same name procedure. A Sprite keeps live state between sessions. This is the primary feature of this backend. Problem 2: The branch was older than the current backend classification pathsTwo commits correct this. Commit 4c4b387 added The five cited files now all include
Thus the prompt does not show host data for a Sprite session. The file tools use the container path rules. The approval layer uses the container rules. Suggested change 1: A stable identity, with tests for resume and for isolationThe tests are in the unit suite. The live test Suggested change 2: Salvage on the current classifications, with tests for those routesThis is complete. See Problem 2 above. The test Other changes
|
What does this PR do?
Adds a new terminal backend,
TERMINAL_ENV=sprites, that runs the agent's shell commands inside a Sprite — a stateful cloud sandbox on Fly.io with native checkpoint & restore.Sprites fills a gap between the existing cloud-sandbox backends. e.g:
Wired through the existing
_create_environmentfactory; no changes to the agent or tool surfaces. Apip install 'hermes-agent[sprites]'plusSPRITES_TOKENis the only user-facing setup, andhermes setup terminalwalks through it.Sprite identity is profile-scoped: sandboxes are named
hermes-{profile}-{task_id}(hermes-{task_id}on the default profile), so independent Hermes profiles never resume into one another's live Sprite, while the same(profile, task_id)always resumes the same one. Within-profile reuse across sessions is the backend's designed behavior — the same naming scheme and task-id resolution the Daytona backend uses on main.Type of Change
Changes Made
tools/environments/sprites.py—SpritesEnvironmentonsprites-py. Spawn-per-call, stdin via heredoc, persistent by default._resolve_sprite_namederives the deterministic, profile-scoped, Fly/DNS-safe Sprite name.terminal_tool.py(dispatch +_CONTAINER_BACKENDScwd sanitization +container_configbuilder, socontainer_persistent: falseactually reaches the backend),environments/__init__.py,file_operations.py.spritesin every shared remote-backend set on current main —agent/prompt_builder.py(_REMOTE_TERMINAL_BACKENDS+ fallback description, so host info is suppressed and the live in-backend probe is used),tools/approval.py(_should_skip_container_guards),file_tools.py,code_execution_tool.py,skills_tool.py.setup.py(menu + token prompt),status.py(SDK/token state),doctor.py(diagnostics),config.py(token status),AGENTS.md(structure tree).sprites-py>=0.5.0,<0.6inlazy_deps.py+pyproject.toml(upper-bounded per the pre-1.0 rule; full live integration suite verified against 0.5.0).test_sprites_environment.py(27 unit, mocked SDK — construction, naming/profile isolation, dispatch wiring, persistence, file sync),test_sprites_terminal.py(8 integration vs live API, gated bySPRITES_TOKEN).README.md(backend count → eight),configuration.md(per-backend section + restricted-token notes),environment-variables.md(SPRITES_TOKENrow),tools.md,security.md,cli-config.yaml.example(Sprites stanza).How to Test
pip install -e . && pip install 'hermes-agent[sprites]'sprite login(orsprite auth setup --token …). Drop it into~/.hermes/.envasSPRITES_TOKEN=….~/.hermes/config.yaml→terminal.backend: sprites,terminal.cwd: /home/sprite,terminal.container_persistent: true.hermes --yolo -z "Run sprite-env info and quote the output". Confirm thesprite_name/sprite_id/boot_idreturned differ from the host's.client.list_sprites()shows the same Sprite (statuswarm) and the filesystem state from the prior run is intact.container_persistent: false, confirmclient.list_sprites()no longer contains the Sprite aftercleanup().HERMES_HOME=~/.hermes/profiles/work), confirm a separatehermes-work-defaultSprite is created and the default profile's Sprite is untouched.Checklist
Code
feat(terminal):,docs(terminal):)test_sprites_environment.py,test_container_cwd_sanitize.py,test_prompt_builder.py,test_approval.py,test_file_tools.py— 500+ tests) and all pass. The fullpytest tests/ -qcurrently has 11 collection errors on unmodified upstream modules (acp/mcp) that reproduce on main.Documentation & Housekeeping
user-guide/configuration.md,reference/environment-variables.md, module docstrings)cli-config.yaml.example(OPTION 7: Sprites cloud execution)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A (no architecture changes; followed the existingBaseEnvironmentcontract)httpx+websockets(both cross-platform) andbash -con the remote Sprite (Linux); same host requirements as the existing Modal/Daytona backends. No new Windows/macOS constraints.Screenshots / Logs
Full agent loop:
Host VM (for comparison) reports
sprite_name=hermes,sprite_id=sprite-8366d184-…,boot_id=16a68ded-…— all distinct, confirming the agent's command ran inside the Sprite the backend created (not on the host).Notes
~/.hermes/cache/remote-syncs/…because their sandboxes are torn down or reset between sessions. Sprites is persistent by default — the ext4 filesystem is the authoritative store and the same Sprite is resumed by(profile, task_id), so a sync-back would be redundant work.api.sprites.dev; the dashboard does not expose user-selectable knobs for those today. The setup wizard surfaces this as a note instead of prompting for ignored values.Cmd.kill()is a no-op. The sprites-py SDK has no public cancel hook on a running WebSocket exec; timeouts use the SDK's WebSocket deadline.Changelog since the automated review
main; profile-scoped Sprite identity (_resolve_sprite_name) +TestSpriteNamingcovering resume, cross-profile isolation, slug sanitization, and resolver-failure fallback; registeredspritesin the then-current backend classification paths (prompt host-suppression/probe, cwd sanitization, container_config builder, approval guard helper) with wiring tests pinning each.main(~8.4k commits, spanning the Vercel Sandbox revert/re-add — resolutions keep upstream's Vercel surface and add Sprites alongside); swept the classification sites added since July (tools/env_probe.py_REMOTE_BACKENDS— the file cited in the review — plusfile_toolsfallback set/backend sniff,terminal_toolenv-parse gate,prompt_builderprobe config,credential_filescache-path translation); modernized thesprites-pypin to the stable 0.5.x series; re-ran the full live integration suite (8/8 green againstapi.sprites.devon sprites-py 0.5.0).