Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Terminal Environment Provider Plugins
|
||||
|
||||
Hermes runs shell commands through a pluggable set of **terminal backends**.
|
||||
The built-in backends (local, Docker, Singularity, Modal, Daytona, Vercel
|
||||
Sandbox, SSH) live in the core repo under `tools/environments/`. Third-party
|
||||
sandbox vendors integrate as **plugins** instead — a standalone plugin repo
|
||||
installed under `~/.hermes/plugins/`, registering a backend the user selects
|
||||
exactly like a built-in one via `terminal.backend` in `config.yaml`.
|
||||
|
||||
This page mirrors the [Browser Provider Plugins](/developer-guide/browser-provider-plugin)
|
||||
guide — same registration flow, same scope semantics.
|
||||
|
||||
## What a provider controls
|
||||
|
||||
A registered backend automatically participates in every core surface:
|
||||
|
||||
| Surface | Driven by |
|
||||
|---|---|
|
||||
| Command dispatch (`terminal`, `execute_code`, file tools) | `create_environment()` |
|
||||
| `hermes setup` backend picker | `display_name`, `description`, `setup_instructions()`, `post_setup()` |
|
||||
| Dashboard terminal-backend picker (probe status) | `probe()` |
|
||||
| `hermes status` / `hermes doctor` | `doctor_checks()` |
|
||||
| System-prompt environment hints | `is_remote`, `env_description` |
|
||||
| Dangerous-command approval skipping | `skip_container_guards` |
|
||||
| Container path/cwd handling | `is_container` |
|
||||
| Synced cache-file path translation | `cache_path_base` |
|
||||
| Secret stripping from spawned subprocesses | `strip_env_keys` |
|
||||
| Per-session sandbox isolation (`container_persistent: false`) | `session_isolated_when_nonpersistent` |
|
||||
|
||||
Declaring these flags on the provider closes the classic "new backend missed
|
||||
classification site N" bug class — the core consults the registry at each
|
||||
site instead of a hardcoded list of names.
|
||||
|
||||
## Minimal provider
|
||||
|
||||
```python title="~/.hermes/plugins/acmebox/__init__.py"
|
||||
from agent.terminal_env_provider import TerminalEnvironmentProvider
|
||||
|
||||
|
||||
class AcmeBoxEnvironment:
|
||||
"""Must satisfy the BaseEnvironment duck-typed contract."""
|
||||
|
||||
def __init__(self, cwd, timeout, task_id):
|
||||
self.cwd, self.timeout, self.task_id = cwd, timeout, task_id
|
||||
|
||||
def execute(self, command, timeout=None, **kwargs):
|
||||
... # run the command in the sandbox
|
||||
return {"output": "...", "exit_code": 0}
|
||||
|
||||
def cleanup(self):
|
||||
... # tear down / detach
|
||||
|
||||
|
||||
class AcmeBoxProvider(TerminalEnvironmentProvider):
|
||||
name = "acmebox"
|
||||
display_name = "AcmeBox"
|
||||
is_remote = True # commands don't run on the host
|
||||
is_container = True # container-style path/cwd semantics
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return "Run commands in an AcmeBox cloud sandbox."
|
||||
|
||||
@property
|
||||
def cache_path_base(self):
|
||||
return "~/.hermes" # where synced cache files land, or None
|
||||
|
||||
@property
|
||||
def strip_env_keys(self):
|
||||
return frozenset({"ACMEBOX_TOKEN"})
|
||||
|
||||
def is_available(self):
|
||||
import importlib.util, os
|
||||
return (
|
||||
importlib.util.find_spec("acmebox") is not None
|
||||
and bool(os.getenv("ACMEBOX_TOKEN"))
|
||||
)
|
||||
|
||||
def create_environment(self, *, cwd, timeout, task_id="default",
|
||||
image=None, container_config=None, **kwargs):
|
||||
return AcmeBoxEnvironment(cwd, timeout, task_id)
|
||||
|
||||
|
||||
def register(ctx):
|
||||
ctx.register_terminal_environment_provider(AcmeBoxProvider())
|
||||
```
|
||||
|
||||
```yaml title="~/.hermes/plugins/acmebox/plugin.yaml"
|
||||
name: acmebox
|
||||
version: 0.1.0
|
||||
description: AcmeBox cloud sandbox terminal backend
|
||||
kind: backend
|
||||
```
|
||||
|
||||
Enable it, select it, run:
|
||||
|
||||
```bash
|
||||
hermes plugins enable acmebox
|
||||
hermes config set terminal.backend acmebox
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **Reserved names.** Registrations that collide with a built-in backend name
|
||||
(`local`, `docker`, `singularity`, `modal`, `managed_modal`, `daytona`,
|
||||
`vercel_sandbox`, `ssh`) are rejected. Plugins extend the backend set; they
|
||||
never shadow in-tree backends.
|
||||
- **`create_environment` must accept `**kwargs`** and ignore unknown keys —
|
||||
the forward-compat contract that lets the factory signature evolve without
|
||||
breaking older plugins.
|
||||
- **`is_available()` / `probe()` must be cheap.** No network calls — they run
|
||||
during requirement checks and UI paints.
|
||||
- **Fail-soft everywhere.** A provider attribute that raises is treated as
|
||||
its default by the core (e.g. a raising `skip_container_guards` keeps the
|
||||
approval layer ON). Don't rely on exceptions for control flow.
|
||||
- **Secrets belong in `strip_env_keys`.** Your vendor token must never be
|
||||
readable by a model-authored shell command; listing it strips it from every
|
||||
spawned subprocess unconditionally, like the built-in `MODAL_*` /
|
||||
`DAYTONA_API_KEY` handling.
|
||||
|
||||
## Environment object contract
|
||||
|
||||
`create_environment()` returns an object satisfying the same duck-typed
|
||||
interface as `tools.environments.base.BaseEnvironment`:
|
||||
|
||||
- `execute(command, timeout=None, ...)` → `{"output": str, "exit_code": int}`
|
||||
- `cleanup()` — release resources; called on session teardown / idle reaping
|
||||
- Optional: persistence hooks mirroring the built-in cloud backends
|
||||
|
||||
Subclassing `BaseEnvironment` is recommended (you inherit the shared file-sync
|
||||
and background-process plumbing) but not required.
|
||||
|
||||
## Session isolation semantics
|
||||
|
||||
If your sandbox is **resumed by name** (a durable VM the backend re-attaches
|
||||
to), set `session_isolated_when_nonpersistent = True`. With
|
||||
`terminal.container_persistent: false`, each session then gets its own
|
||||
sandbox identity instead of sharing one — without this, two independent
|
||||
ephemeral runs could attach one live VM and delete it out from under each
|
||||
other.
|
||||
Reference in New Issue
Block a user