Skip to main content
Every 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 (nativesandlock) 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:
The same rule applies when a managed backend holds a 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

A sandbox=True agent runs tools in a separate process — but a separate process is not a security boundary.
Note: a separate process is not a security boundary — it can still reach the network and read your files. Use docker or a cloud provider for untrusted code.
The network stays reachable and ~/.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 from sandbox=, 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.
Compare local, shared-sandbox, and hosted from one script:

Best Practices

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.
run_on= moves the tools out and shares one sandbox across steps. A hosted backend= moves the whole loop off this machine. The two shapes solve different problems — the repr tells them apart.
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.
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).

Sandbox

Per-agent sandbox= and the explicit execute_code() API

Shared Sandbox

run_on= gives a whole flow one shared sandbox

Sandboxed Agent

Local loop, sandboxed tools

Self-Hosted Agent (Docker)

run_on="docker" — the whole loop self-hosted

Hosted Agent

Run the whole agent loop on a provider’s cloud

Local Agent

Run agent loops locally with any LLM