Agent, AgentTeam, and AgentFlow can tell you — from repr() alone or via where_does_it_run() — where the model runs and where the tools run.
Quick Start
1
Ask an agent where it runs
A plain agent thinks and runs its tools on this machine:
2
A workflow with a shared sandbox
Set
run_on="docker" and every step shares one sandbox:3
Ask a hosted agent where it runs
A hosted backend moves the whole turn to the provider’s cloud:
4
Read the answer as a dictionary
Three topologies at a glance
tools_run_on= moves the tools out but keeps the model call local. A whole-loop backend moves both off this machine — hosted on a vendor’s cloud, or self-hosted in a container you own. The repr names both places, so you can tell them apart at a glance.
docker on both sides uses the same default image (python:3.12-slim), so run_on="docker" and tools_run_on="docker" hand you the same Python runtime. Pass an explicit image= to override.There is no
HostedAgent class to import today. When a backend= object reports its provider as anthropic (its class name contains Hosted or Anthropic and it exposes a .provider), both places become Anthropic's cloud. You can see the exact phrasing without any backend using say_place:How the object knows
repr() asks a single shared helper — describe() — which reads three attributes off the object and turns provider names into plain phrases. where_does_it_run() passes the same result through explain() to produce sentences.
describe() never raises — if a property throws, thinks_on falls back to this machine so that describing where something runs can never break the thing it describes.
_backend_places() now asks the backend via provider_name rather than name-matching the class. A self-hosted backend (like run_on="docker") used to be misreported as thinking on this machine because its class name did not contain Hosted or Anthropic; asking it what it is fixes that — the whole-loop container is now reported as a Docker container for both places.
_backend_places() reads a backend’s _compute / compute with via="compute", because that attribute is a compute-registry provider — otherwise a LocalCompute here would borrow the subprocess backend’s stronger words. See The two flavors of local.
As of PR #4070,
Agent(tools_run_on="native") (and any alias in _compute_bridge._ALIASES) is accepted at construction. Validation used to run before alias resolution, so a spelling the resolver would happily route raised TypeError: 'native' is not a known place. tool_places() now includes the alias table it validates against.As of PR #4092, the vendor compute providers live in the standalone
praisonai-sandbox package (same names, same behaviour; old praisonai.integrations.compute.* imports still work through a shim).What each place name means
Provider names map to phrases a non-developer can read. These are the exact strings the object returns.
Public aliases resolve to the backend that actually runs — the alias name never appears in the output:
An unknown provider falls back to its own name:
say_place("some-new-cloud") returns "some-new-cloud".
As of PR #4070,
Agent(tools_run_on="native") — and any alias in _compute_bridge._ALIASES — is accepted at construction. Validation used to run before alias resolution, so a spelling the resolver would happily route (native → sandlock) raised TypeError: 'native' is not a known place. tool_places() now includes the alias table it validates against, so native resolves to sandlock and reports tools_run_on='a locked-down process on this machine'.native is a real boundary, consistent with sandlock (PR #4107). native is an alias for sandlock — Landlock + seccomp + a scrubbed environment + deny-all networking. Before PR #4107, _NOT_A_BOUNDARY in execution_location.py was {"subprocess", "local", "native"}, so the two spellings of the same backend gave opposite security advice: tools_run_on="native" triggered the “not a security boundary” warning while tools_run_on="sandlock" did not. _NOT_A_BOUNDARY is now {"subprocess", "local"} — native is reported as a boundary, matching sandlock.The two flavors of local
local means two different backends depending on which parameter carried it, so the phrase depends on where the word came from.
run_in="local"→ SandboxManager collapses it to the subprocess backend (scrubbed environment, blocked commands and paths) →"a separate process on this machine"tools_run_on="local"→ the wrapper’s LocalCompute: a plain shell in the current directory, full environment inherited, no security policy →"a plain shell on this machine (no policy applied)"
say_place(name, *, via="sandbox") normalises the provider object first, then reads the compute-only word table. Pass via="compute" for the tools_run_on= reading:
LocalCompute provider object (provider_name="local"). A backend’s _compute / compute is a compute-registry provider, so _backend_places() reads it with via="compute" — otherwise a LocalCompute here would borrow the subprocess backend’s stronger words.
Behaviour-change note (PR #4070). Upgrading changes the printed string for a managed backend holding a
LocalCompute, not the runtime behaviour. The previous string understated the shell’s freedom and overstated the sandbox’s protection.The subprocess warning
Asandbox=True agent runs tools in a separate process — but a separate process is not a security boundary.
~/.ssh stays readable under a subprocess. A real container (run_on="docker") is a real boundary, so it does not trigger this note. See Sandbox for the same warning in depth.
Configuration Options
This page adds no new knobs — where things run comes fromsandbox=, run_on=, and backend=, documented on the sandbox pages below. The only new vocabulary is the field names the object returns:
The field is
thinks_on / tools_run_on, never loop — that word already means the workflow loop() primitive, asyncio’s event loop, and agent-loop jargon, so it was deliberately rejected.
Public API surface
Three ways to read the answer, from a quick glance to structured data:
The helper functions live under
praisonaiagents.agent.execution_location:
Common Patterns
Check where things run before a paid run, log it at startup, or assert it in a test.Best Practices
Print the object, don't guess where it runs
Print the object, don't guess where it runs
The repr is now the primary source of truth.
repr(agent) names both places, so you never have to consult docs to learn whether tools run locally or in a container.Believe the 'not a security boundary' note
Believe the 'not a security boundary' note
Under a subprocess the network is reachable and
~/.ssh is readable. Use docker or a cloud provider for untrusted code — the note appears precisely because that isolation is bypassable.The warning fires for subprocess / local only — never for sandlock / native, docker, or any hosted provider. Since PR #4107, native (an alias for sandlock) is correctly excluded, so tools_run_on="native" and tools_run_on="sandlock" give the same advice.Call describe() for programmatic access
Call describe() for programmatic access
The string format is for humans. For the raw dict, call
describe(obj) from praisonaiagents.agent.execution_location — it returns {thinks_on, tools_run_on, code_runs_on?} and never raises.Know that native and local are aliases
Know that native and local are aliases
native and local are public aliases. The sentences report the backend they resolve to (sandlock and subprocess), which is the source of truth. native resolving to sandlock means it is a real security boundary — the “not a security boundary” note never fires for it (fixed in PR #4107).Related
Sandbox
Per-agent
sandbox= and the explicit execute_code() APIShared Sandbox
run_on= gives a whole flow one shared sandboxSandboxed Agent
Local loop, sandboxed tools
Self-Hosted Agent (Docker)
run_on="docker" — the whole loop self-hostedHosted Agent
Run the whole agent loop on a provider’s cloud
Local Agent
Run agent loops locally with any LLM

