Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ or one of them only where it is used.
- Defaults are local and carry no external coordinates: SQLite document store, filesystem object
store, an OCI registry at `localhost:5000`, env-var secret store, `LocalRunner`, `local` Docker
sandboxes for envs and agents. MongoDB, S3, Cloud Storage, ECR, AWS Secrets Manager, Google Cloud
Secret Manager, Modal, E2B and Tensorlake exist as implementations and are selected by config
(Tensorlake through the `tensorlake` extra).
Secret Manager, Modal, E2B, Tensorlake and Tenki exist as implementations and are selected by config
(Tensorlake through the `tensorlake` extra, Tenki through the `tenki` extra).
- The model endpoint is unset until `[model] base_url` / `api_key` (or `LITELLM_BASE_URL` /
`LITELLM_API_KEY`) is configured. A loopback URL handed into a `local` container (the model
endpoint, an agent, peer or gateway on this machine) becomes `host.docker.internal`. On Linux that
Expand Down Expand Up @@ -96,7 +96,7 @@ and deserialized through a registry.
| `task/`, `task_step/` | A `Task` holds its `TaskStep`s inline; `Task.run()` executes them as a DAG. `depends_on` (None means all prior steps) gates a step, independent steps run concurrently, `fail_task_on_error` makes a failure fatal or tolerated, `retry_config` rolls a failed span back through the step journal and re-dispatches it. Built-in steps live in `task_step/task_steps/` (`deploy_env`, `deploy_agent`, `prompt_agent`, the verifiers under `verifiers/`, and more). |
| `store/` | Four store ABCs with local and cloud implementations: `DocumentStore` (SQLite, MongoDB), `ObjectStore` (filesystem, S3, Cloud Storage), `ImageStore` (local OCI registry, ECR), `SecretStore` (env vars or file, AWS Secrets Manager, Google Cloud Secret Manager). `VersionedEntityStore` implements the shared versioned get/put logic, `QueryBuilder` is the immutable chained query API, `store/base.py` holds the error types. A new backend must pass the conformance kits in `tst/store/`. |
| `config/` | The `Config` singleton (`get_config`, `configure`, `reset_config`) in `config/runtime.py`, file discovery in `config/loader.py`, and `load_impl`, which resolves `module:Class` pointers. `agent_env.store` re-exports the config names for compatibility. |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra), `tensorlake` (the `tensorlake` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, and for VMs on this machine the address their requests to it arrive at, `REACHES_HOST_AT`; `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra), `tensorlake` (the `tensorlake` extra), `tenki` (the `tenki` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, and for VMs on this machine the address their requests to it arrive at, `REACHES_HOST_AT`; `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store) and `EnvironmentGatewayProvider`, which renders a docker-compose for the gateway and its MCP servers inside the sandbox; `providers/env_state/` holds env-state providers (`local_postgres` built in). |
| `a2a_agent/` | The `A2AAgent` entity (`a2a_agent`), its stores and the validator steps. The protocol package provides the agent-side framework. |
| `runner/` | The `[runner]` seam: `Runner.submit()` returns `(run_id, instance_id)`; `LocalRunner` is built in. |
| `explorer/` | Optional local web UI: `agent-env up`, needs the `explorer` extra, binds loopback `:8234`. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Or install it with pip, into a virtualenv:
pip install agentenv-framework
```

To use the SDK in your own project, add it as a dependency with `uv add agentenv-framework`. The explorer, `agent-env up`, needs the `explorer` extra (`uv tool install 'agentenv-framework[explorer]'`, or the same extra with uvx or pip) and an `.agentenv/config.toml` in the current folder or above it; an empty one keeps every local default. The `tensorlake` sandbox backend needs the `tensorlake` extra (`pip install 'agentenv-framework[tensorlake]'`) and an `api_key` in `[sandbox.providers.tensorlake.config]`. Its sandboxes boot from the public image `agentenv-dind-host-v1`; set `image` there to use another registered Docker-capable image.
To use the SDK in your own project, add it as a dependency with `uv add agentenv-framework`. The explorer, `agent-env up`, needs the `explorer` extra (`uv tool install 'agentenv-framework[explorer]'`, or the same extra with uvx or pip) and an `.agentenv/config.toml` in the current folder or above it; an empty one keeps every local default. The `tensorlake` sandbox backend needs the `tensorlake` extra (`pip install 'agentenv-framework[tensorlake]'`) and an `api_key` in `[sandbox.providers.tensorlake.config]`. Its sandboxes boot from the public image `agentenv-dind-host-v1`; set `image` there to use another registered Docker-capable image. The `tenki` sandbox backend needs the `tenki` extra (`pip install 'agentenv-framework[tenki]'`) and an `api_key` in `[sandbox.providers.tenki.config]`; see [its README](src/agent_env/providers/sandbox_providers/tenki/README.md).

A plain install runs on local stores and needs no cloud SDK. The cloud store backends are extras: `aws` for S3, Secrets Manager, DynamoDB and ECR (`pip install 'agentenv-framework[aws]'`), and `gcp` for Cloud Storage, Secret Manager, Firestore and Artifact Registry (`pip install 'agentenv-framework[gcp]'`). A config that names a backend without its extra fails, naming the extra to install.

Expand Down
9 changes: 8 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ marks, or contributor names to endorse or promote Scale AI, or related products.

## Python dependencies

The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp`, `sail` and `tensorlake` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed.
The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp`, `sail`, `tensorlake` and `tenki` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed.

### a2a-sdk 0.3.26

Expand Down Expand Up @@ -716,6 +716,13 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `explorer`, `gcp
- Source: <https://pypi.org/project/synchronicity/0.12.6/>
- [License text 1](#license-text-1)

### tenki 1.4.1

- License: MIT
- Author: Tenki Cloud
- Source: <https://pypi.org/project/tenki/1.4.1/>
- License text: not shipped with the package; see its source

### tensorlake 0.5.148

- License: Apache-2.0
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ sail = [
tensorlake = [
"tensorlake~=0.5.140", # AsyncSandbox APIs validated against 0.5.140 to 0.5.149
]
# The Tenki Sandbox VM provider, selected as the built-in `tenki` backend.
tenki = [
"tenki[async]~=1.4.1", # AsyncClient/AsyncSandbox exec, stdin, ports and egress APIs validated against the 1.4 line
]
dev = [
"agentenv-framework[aws,explorer,gcp,sail,tensorlake]", # so the AWS, explorer, GCP backend, Sail and Tensorlake provider tests run in CI
"agentenv-framework[aws,explorer,gcp,sail,tensorlake,tenki]", # so the AWS, explorer, GCP backend, Sail, Tensorlake and Tenki provider tests run in CI
"fastmcp>=3.4,<4", # the standalone-fastmcp mount path of the protocol; <4 keeps mcp 1.x
"griffelib==2.3.0", # the plugin API check; exact, since what it reports as a break changes between releases
"moto>=5.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/a2a_agent/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@click.option("--ttl-seconds", type=click.IntRange(min=MIN_TTL_SECONDS, max=MAX_TTL_SECONDS), default=DEFAULT_TTL_SECONDS,
help=f"VM lifetime in seconds (default {DEFAULT_TTL_SECONDS})")
@click.option("--sandbox", default=None,
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, local) "
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, tenki, local) "
"or a name from [sandbox.providers] in .agentenv/config.toml; comma-separated for a "
"fallback chain. Defaults to [sandbox].agent_default (else local) when omitted.")
def deploy(agent_id: str, agent_version: int | None, env_var_pairs: tuple[str, ...], ttl_seconds: int, sandbox: str):
Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/cli/env/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
@click.option("--gateway-mode", type=click.Choice([m.value for m in GatewayMode], case_sensitive=False),
default=GatewayMode.PERFORMANCE.value, help="Gateway mode (performance or consistent)")
@click.option("--sandbox", default=None,
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, local) "
help="Sandbox backend(s): a built-in (modal, modal_vm, e2b, tensorlake, tenki, local) "
"or a name from [sandbox.providers] in .agentenv/config.toml; comma-separated for a "
"fallback chain. Defaults to [sandbox].default (else local) when omitted.")
@click.option("--service-db", "service_db_env_id", default=None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ def effective_network_policy(cls, policy: Optional[NetworkPolicy]) -> NetworkPol
"e2b": "agent_env.providers.sandbox_providers.e2b:E2BSandboxProvider",
"sail_vm": "agent_env.providers.sandbox_providers.sail_vm.provider:SailVmSandboxProvider",
"tensorlake": "agent_env.providers.sandbox_providers.tensorlake.provider:TensorlakeSandboxProvider",
"tenki": "agent_env.providers.sandbox_providers.tenki.provider:TenkiSandboxProvider",
"local": "agent_env.providers.sandbox_providers.local_sandbox:LocalSandboxProvider",
}

Expand Down
156 changes: 156 additions & 0 deletions src/agent_env/providers/sandbox_providers/tenki/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# `tenki` sandbox provider

Runs agent-env sandboxes on [Tenki Sandbox](https://tenki.cloud/docs/sandbox) VMs. Each environment and each agent
gets its own Linux VM, with Docker inside and public HTTPS URLs for its ports. It is a VM provider: agent-env's
docker-in-VM flows run on it unchanged. That covers the gateway's docker-compose, an agent's `docker run`, image loading
and artifact collection.

## Set up

### 1. Install the extra

The Tenki SDK is an optional extra:

```bash
pip install 'agentenv-framework[tenki]' # or: uv add 'agentenv-framework[tenki]'
```

Without it, selecting `tenki` fails with a `ConfigError` that names this extra. Nothing else needs it.

### 2. Get a Tenki API key

Create a key as described in [API keys](https://tenki.cloud/docs/account/api-keys). For local work, export it:

```bash
export TENKI_API_KEY=tk_...
```

For a shared deployment, put it in your secret store instead, for example as `tenki_api_key`, and reference it with
`secret:tenki_api_key`. Never put the key itself in `config.toml`. The provider reads the key only from its config,
never from the process environment.

### 3. Configure

A complete `.agentenv/config.toml` that runs every environment and agent on Tenki, with the local stores:

```toml
[sandbox]
default = "tenki" # environments and sandboxes
agent_default = "tenki" # agents

[sandbox.providers.tenki.config]
api_key = "env:TENKI_API_KEY" # or "secret:tenki_api_key"

[model]
base_url = "https://litellm.example.com"
api_key = "env:LITELLM_API_KEY"
```

To keep the local default and use Tenki per run instead, add only the `[sandbox.providers.tenki.config]` table and
pass `--sandbox tenki`, or `--env-sandbox tenki` / `--agent-sandbox tenki` on `agent-env task run`.
`agent-env config show` prints the file in effect and masks the key.

### 4. Run something

The bundled `hello` task deploys a sandbox, loads a file into it and checks it, with no model needed:

```bash
agent-env run hello --sandbox tenki
```

`agent-env -v run …` also logs each VM as it starts
(`Tenki VM started: session_id=… cpu=… memory=…MB disk=…GB ports=… attribution=…`).

## Configuration reference

All keys go under `[sandbox.providers.tenki.config]`. Unknown keys are refused.

| Key | Default | Meaning |
| ------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `api_key` | required | The Tenki API key, as an `env:` or `secret:` reference. |
| `base_url` | `https://api.tenki.cloud` | The Tenki API endpoint. |
| `snapshot_id` | none | Boot every VM from this snapshot, for example one with Docker already installed (see below). |
| `image` | the Tenki base image | Boot every VM from this Tenki registry image instead. At most one of `image` and `snapshot_id`. |
| `min_disk_gb` | `20` | The smallest VM disk. Docker keeps compressed and extracted layers, so agent-env's 10 GB default is raised. |

## Faster startup

A VM booted from the Tenki base image installs Docker and Compose v2 the first time agent-env uses it. Snapshot a VM
that has them once, and every VM booted from the snapshot skips that step:

```bash
tenki sandbox create --name agentenv-docker --cpu 2 --memory-mb 4096 --disk-size-gb 20 --max-duration 30m
tenki sandbox exec --session agentenv-docker -c 'sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io docker-compose-v2 docker-buildx curl'
tenki sandbox exec --session agentenv-docker -c 'sudo systemctl start docker && sudo docker info --format "{{.ServerVersion}}"'
tenki sandbox snapshot create agentenv-docker --name agentenv-docker --wait-durable
tenki sandbox terminate agentenv-docker
```

Then set `snapshot_id` to the snapshot's id. A VM can be restored larger than its snapshot but not smaller, so keep
the snapshot's VM at 2 vCPU and 4 GiB, the smallest the provider starts. Images your environments and agents pull
from a registry can be pulled into the same VM before the snapshot; a VM booted from it then finds them in place.

## Resources and lifetime

- **CPU and memory:** whole vCPUs, memory rounded up to 2 MiB. A request below 2 vCPU or 4 GiB is raised to it, the
smallest VM Tenki starts from a warm snapshot.
- **Disk:** at least `min_disk_gb`.
- **Refused requests:** a request beyond Tenki's limits (128 vCPU, 512 GiB memory, 100 GB disk, or the workspace's own
lower limit) is refused before anything is created, not shrunk.
- **Ignored requests:** `boot_mode` has no Tenki equivalent, so a `boot_mode` that is set logs a warning and has no
effect.
- **Lifetime:** the sandbox's `timeout` is the VM's hard maximum lifetime, so a crashed run can't leave a VM running
past it. It is also every command's execution budget. A workspace with a lower maximum session duration grants that
instead; the provider logs a warning and budgets commands by the lifetime granted.

## Networking

- **Ports:** each exposed port gets a public HTTPS URL under `sb.tenki.sh`, which fills `tunnel_urls`. It serves HTTP
and WebSocket with no platform authentication.
- **Egress:** allow-all, or an allowlist of up to 256 hostnames and 64 IPv4 CIDRs. An IP address listed as a host
becomes a /32. A policy with IPv6 or malformed entries is refused with `NetworkPolicyUnsupportedError` before
anything is created, and a fallback chain skips to its next provider. Tenki applies the policy outside the VM, so
Comment on lines +111 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Docs overstate hostname checks

The README promises that all malformed entries raise NetworkPolicyUnsupportedError before creation. But supports_network_policy only checks CIDRs and entry counts. A malformed hostname passes those checks and reaches client.create, so users can receive an SDK or API error instead of the promised early rejection.

Narrow the wording to malformed CIDRs, or add hostname checks and tests.

Suggested change
becomes a /32. A policy with IPv6 or malformed entries is refused with `NetworkPolicyUnsupportedError` before
anything is created, and a fallback chain skips to its next provider. Tenki applies the policy outside the VM, so
becomes a /32. A policy with IPv6 or malformed CIDRs is refused with `NetworkPolicyUnsupportedError` before
anything is created, and a fallback chain skips to its next provider. Tenki applies the policy outside the VM, so
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/providers/sandbox_providers/tenki/README.md
Line: 111-112

Comment:
**Docs overstate hostname checks**

The README promises that all malformed entries raise `NetworkPolicyUnsupportedError` before creation. But `supports_network_policy` only checks CIDRs and entry counts. A malformed hostname passes those checks and reaches `client.create`, so users can receive an SDK or API error instead of the promised early rejection.

Narrow the wording to malformed CIDRs, or add hostname checks and tests.

```suggestion
  becomes a /32. A policy with IPv6 or malformed CIDRs is refused with `NetworkPolicyUnsupportedError` before
  anything is created, and a fallback chain skips to its next provider. Tenki applies the policy outside the VM, so
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

one policy covers `dockerd` and every container.
- **Wildcards:** `*.example.com` covers exactly one label: `files.example.com`, not `a.b.example.com`. List deeper names
explicitly.
- **Tenki's own hosts:** `*.sb.tenki.sh` and `*.us.sb.tenki.sh` are added to every allowlist, so a restricted agent can
reach the environments it talks to. A workspace with wildcard preview domains gets URLs one label deeper
(`<token>.<workspace>.us.sb.tenki.sh`); the provider adds `*.<workspace>.us.sb.tenki.sh` once it has exposed a port
there. A restricted VM created before any environment in the run needs that entry in its own `allow_hosts`.
- **Image and object downloads:** Tenki sets a VM's egress when it is created. Objects and image tarballs whose host the
allowlist doesn't cover go over the exec's stdin instead, which needs no egress. Image pulls and builds inside the
VM still need their registries allowed, and so does installing Docker on the base image, so boot allowlisted VMs
from a snapshot that has Docker.
- **Reconnect:** `get_sandbox` restores the ports and the applied egress policy.

## Attribution and cost

Every VM is tagged `agentenv` and carries agent-env's attribution, including `[sandbox.attribution]` defaults, in its
session metadata. `tenki sandbox list --tags agentenv` lists them, and `tenki sandbox get <session id> --json` shows a
VM's metadata.

## Cleanup

- A VM whose setup fails after it is created (readiness, ports, Docker) is terminated.
- If the caller is cancelled while the create is in flight, the provider terminates the VM that create makes once it
answers.
- If a create fails after Tenki may have made the VM, the provider finds that VM by its unique name and terminates it.
- `terminate` is idempotent: a VM that is already gone or terminating is not an error.

## Things to know

- **Commands run as root** through Tenki's privileged exec, so the `sudo` agent-env prefixes is dropped.
- **Lost connections:** when a command's stream ends without an exit status, `exec` returns exit code -1.
`exec_script` retries idempotent scripts on -1 only.
- **Images:** `create_vm(image=...)` is refused. VMs boot the configured `image` or `snapshot_id`, and OCI images run
under Docker inside them.
- **GPUs** are not supported.
- **The model key enters the sandbox:** an agent's `LITELLM_API_KEY` is passed in, as on other providers.

## Testing

`tst/integration/providers/sandbox_providers/tenki_sandbox_smoke_test.py` runs against a real Tenki workspace when
`[sandbox.providers.tenki.config]` resolves, for example with `make int-test-slow`. Otherwise it skips with
`agentenv-capability-missing: remote_sandbox`. The unit tests are
`tst/unit/providers/sandbox_providers/tenki_sandbox_provider_test.py` and `tenki_sandbox_test.py`, and run offline
against in-memory stand-ins for the SDK.
Loading
Loading