Sandbox a tool call¶
What this recipe does¶
Runs a command inside a SandboxManager with a scrubbed environment,
an allowlist of env keys, and an explicit overlay. The local-subprocess
backend needs no Docker, so the demo runs anywhere Python does.
When to use¶
- You're invoking an
ExecutableUnitNodewhose manifest calls out to an external binary and you want it isolated from the parent env. - You want deterministic, cache-keyed environment preparation so repeated runs reuse the same prepared sandbox.
- You need to restrict which env variables a tool sees (e.g. strip
AWS_*keys before running untrusted code).
When NOT to use¶
- The tool is pure Python and can run in-process — you save the subprocess overhead by calling it directly.
- You need full container isolation — use the Docker backend
(
SandboxBackendMode.DOCKER) instead ofLOCAL.
Recipe¶
"""23 — Secrets resolved through a real SecretResolver + in-process sandbox run.
What this shows
---------------
Two real primitives stitched together:
* :class:`SecretResolver` + :class:`EnvSecretProvider` turn a list of
:class:`EnvironmentVariable` records (some plain, some ``secret_ref``)
into a concrete env dict — the same path the runner uses when
building the env for a sandboxed unit.
* :class:`SandboxManager` runs a short ``python -c`` command inside
the ``local`` sandbox backend with that env. The env is scrubbed to
an allowlist and the secret arrives inside the subprocess.
* :class:`SecretRedactor` (via ``resolver.redactor()``) is the same
redactor the audit pipeline uses to strip secret values out of
payloads before they are persisted.
Run
---
uv run python examples/23_secrets_and_sandbox.py
"""
from __future__ import annotations
# Allow python examples/NN_name.py to find the sibling examples/_common.py helper.
import sys as _sys
from pathlib import Path as _Path
_sys.path.insert(0, str(_Path(__file__).resolve().parents[1]))
import os
import sys
from zeroth.integrations.execution import (
EnvironmentVariable,
SandboxBackendMode,
SandboxConfig,
SandboxManager,
)
from zeroth.platform.secrets import EnvSecretProvider, SecretResolver
def main() -> int:
# 1. Provider seeded with two secret refs. In production this is a
# vault-backed provider; here we inject a dict so the example is
# hermetic. No real secrets live in the file.
provider = EnvSecretProvider(
environment={
"secret/demo/api_key": "sk-demo-zeroth-23-example-1234",
"secret/demo/db_password": "p@ssw0rd-demo",
}
)
resolver = SecretResolver(provider)
# 2. EnvironmentVariable records mix plain values and secret refs.
# This is exactly the shape an ExecutableUnitManifest carries in
# its ``environment_variables`` field.
env_vars = [
EnvironmentVariable(name="DEMO_MODE", value="cookbook"),
EnvironmentVariable(name="DEMO_API_KEY", secret_ref="secret/demo/api_key"),
EnvironmentVariable(name="DEMO_DB_PASSWORD", secret_ref="secret/demo/db_password"),
]
resolved = resolver.resolve_environment_variables(env_vars)
print("resolved env (full):")
for key, value in resolved.items():
print(f" {key}={value}")
# 3. Run a real command inside SandboxManager's local backend. The
# allowlist keeps PATH; the overlay adds our demo env on top.
manager = SandboxManager(
base_env={
"HOME": os.environ.get("HOME", "/tmp"),
"PATH": os.environ.get("PATH", ""),
},
config=SandboxConfig(backend=SandboxBackendMode.LOCAL),
)
result = manager.run(
["python", "-c", "import os; print(os.environ.get('DEMO_API_KEY', 'missing'))"],
allowed_env_keys=["PATH"],
overlay_env=resolved,
cache_identity={"example": "secrets-sandbox"},
timeout_seconds=10,
)
print(f"\nsandbox returncode={result.returncode} stdout={result.stdout.strip()!r}")
assert result.returncode == 0
assert result.stdout.strip() == "sk-demo-zeroth-23-example-1234"
# 4. Redactor strips the resolved secret out of an audit payload
# before it reaches the audit store. This is the same redactor
# the framework uses automatically in production.
audit_payload = {
"tool": "http-post",
"headers": {"Authorization": f"Bearer {resolved['DEMO_API_KEY']}"},
"body": f"password={resolved['DEMO_DB_PASSWORD']}",
}
redacted = resolver.redactor().redact(audit_payload)
print("\nredacted audit payload (what the audit store sees):")
print(f" {redacted}")
assert "sk-demo-zeroth-23-example-1234" not in str(redacted)
assert "p@ssw0rd-demo" not in str(redacted)
print("\nsecrets-and-sandbox demo OK")
return 0
if __name__ == "__main__":
sys.exit(main())
How it works¶
SandboxManager.prepare_environment computes a cache key from the
runtime metadata and the allowlist, then builds (or reuses) a
SandboxEnvironment containing only the allowed variables plus the
overlay. SandboxManager.run dispatches the command through the
configured backend — local subprocess here — and returns a
SandboxExecutionResult with stdout, stderr, exit code, and timing.