Where Your Agent Runs

Ask any agent, team, or workflow where its thinking and its tools actually execute — from the REPL, no docs required

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.

python
from praisonaiagents import Agent

agent = Agent(name="builder")
print(agent.where_does_it_run())
# Thinking (the AI model calls) happens on this machine.
# Tools run on this machine.
# Nothing is isolated: tools you pass run in this program, with your permissions.
Diagram

A self-hosted container

🐳 Agent

🐳 tools

A provider’s cloud

🤖 Agent

☁️ tools

A shared sandbox

🤖 Agent

🐳 tools

This machine

🤖 Agent

🔧 tools

thinks_on: this machine
tools_run_on: this machine

thinks_on: this machine
tools_run_on: a Docker container (shared)

thinks_on: Anthropic’s cloud
tools_run_on: Anthropic’s cloud

thinks_on: a Docker container
tools_run_on: a Docker container

Quick Start

  1. Ask an agent where it runs

    A plain agent thinks and runs its tools on this machine:

    python
    from praisonaiagents import Agent
    
    agent = Agent(name="builder")
    print(repr(agent))
    # Agent(name='builder', thinks_on='this machine', tools_run_on='this machine')
    
    print(agent.where_does_it_run())
    # Thinking (the AI model calls) happens on this machine.
    # Tools run on this machine.
    # Nothing is isolated: tools you pass run in this program, with your permissions.
  2. A workflow with a shared sandbox

    Set run_on="docker" and every step shares one sandbox:

    python
    from praisonaiagents import Agent, AgentFlow
    
    writer = Agent(name="writer")
    reader = Agent(name="reader")
    
    flow = AgentFlow(run_on="docker", steps=[writer, reader])
    print(repr(flow))
    # AgentFlow(name='AgentFlow', steps=2,
    #           thinks_on='this machine',
    #           tools_run_on='a Docker container (shared)')
    
    print(flow.where_does_it_run())
    # Thinking (the AI model calls) happens on this machine.
    # Tools run on a Docker container (shared).
    # Every step shares that same sandbox, so a file written by one step is visible to the next.
  3. Ask a hosted agent where it runs

    A hosted backend moves the whole turn to the provider’s cloud:

    python
    from praisonaiagents import Agent
    from praisonai import HostedAgent
    
    agent = Agent(name="teacher", backend=HostedAgent(provider="anthropic"))
    print(agent.where_does_it_run())
    # Thinking (the AI model calls) happens on Anthropic's cloud.
    # Tools run on Anthropic's cloud.
  4. Read the answer as a dictionary
    python
    from praisonaiagents import Agent, AgentFlow
    from praisonaiagents.agent.execution_location import describe
    
    describe(Agent(name="builder"))
    # {'thinks_on': 'this machine', 'tools_run_on': 'this machine'}
    
    describe(AgentFlow(run_on="docker", steps=[Agent(name="w")]), shared=True)
    # {'thinks_on': 'this machine', 'tools_run_on': 'a Docker container (shared)'}

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.

TopologyExamplethinks_ontools_run_on
Tools remote, loop localAgent(tools_run_on="docker")this machinea Docker container
Whole loop hostedAgent(run_on="anthropic")Anthropic's cloudAnthropic's cloud
Whole loop self-hostedAgent(run_on="docker")a Docker containera Docker container

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.

Diagram
explain()describe()repr_fields()Agent / AgentFlowUserexplain()describe()repr_fields()Agent / AgentFlowUserrepr(obj)repr_fields(obj)describe(obj)read backend, run_on, sandbox_config{thinks_on, tools_run_on, code_runs_on?}key=’value’ pairsAgent(name=’builder’, thinks_on=’…’, tools_run_on=’…’)where_does_it_run()read backend, run_on, sandbox{thinks_on, tools_run_on}build sentencesplain-English answer

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.


What each place name means

Provider names map to phrases a non-developer can read. These are the exact strings the object returns.

ProviderPhrase
localthis machine
subprocessa separate process on this machine
sandlocka locked-down process on this machine
dockera Docker container
e2ban E2B cloud sandbox
modala Modal cloud sandbox
daytonaa Daytona cloud sandbox
flyioa Fly.io machine
tenkia Tenki cloud sandbox
novitaa Novita cloud sandbox
ssha remote machine over SSH
anthropicAnthropic’s cloud

Public aliases resolve to the backend that actually runs — the alias name never appears in the output:

You typeIt reports
nativea locked-down process on this machine
local (in run_in=)a separate process on this machine
local (in tools_run_on=)a plain shell on this machine (no policy applied)

An unknown provider falls back to its own name: say_place("some-new-cloud") returns "some-new-cloud".

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:

python
from praisonaiagents.agent.execution_location import say_place

say_place("local")                 # "a separate process on this machine"
say_place("local", via="compute")  # "a plain shell on this machine (no policy applied)"

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.

python
# A managed backend that carries a LocalCompute provider object
# (provider_name="local") is a COMPUTE-registry provider, so it is
# read with the compute vocabulary — never described in the subprocess
# backend's stronger words.
#
# Before PR #4070: tools_run_on='a separate process on this machine'  (WRONG — overstated)
# After  PR #4070: tools_run_on='a plain shell on this machine (no policy applied)'  (CORRECT)

The subprocess warning

A sandbox=True agent runs tools in a separate process — but a separate process is not a security boundary.

python
from praisonaiagents import Agent

agent = Agent(name="A", sandbox=True)
print(agent.where_does_it_run())
# Thinking (the AI model calls) happens on this machine.
# Tools run on a separate process on this machine.
# Nothing is isolated: tools you pass run in this program, with your permissions.
# 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:

FieldWhen it appearsMeaning
thinks_onalwaysWhere the AI model calls happen
tools_run_onalwaysWhere the tools execute
code_runs_onwhen a sandbox_config existsWhere execute_code() runs

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:

MethodReturnsWhen to use
repr(obj)one line, key=value pairsquick REPL scan
obj.where_does_it_run()plain-English sentenceswhen you want a colleague-friendly explanation
describe(obj) (advanced){thinks_on, tools_run_on, code_runs_on?} dictwhen you need structured values for a UI/log
SymbolKindReturnsNotes
Agent.where_does_it_run()methodstrSentences with the “Nothing is isolated” tail for a fully-local agent
AgentTeam.where_does_it_run()methodstrAdds “Every step shares that same sandbox…” when run_on= is set
AgentFlow.where_does_it_run()methodstrSame shared-sandbox semantics as AgentTeam
`repr(Agent \AgentTeam \AgentFlow)`dunderstrIncludes name=, agents=/steps= (team/flow), thinks_on=, tools_run_on=
describe(obj, *, shared=False)functionDict[str, str]Structured form of the answer — never raises
explain(obj, *, shared=False)functionstrSentence form — same output as .where_does_it_run()
say_place(name)functionstrProvider name → human phrase (see the mapping table)
repr_fields(obj, *, shared=False)functionstrkey='value' pairs used inside custom __repr__s

The helper functions live under praisonaiagents.agent.execution_location:

python
from praisonaiagents.agent.execution_location import describe, explain, say_place

say_place("docker")
# 'a Docker container'

Common Patterns

Check where things run before a paid run, log it at startup, or assert it in a test.

python
from praisonaiagents import Agent, AgentFlow
import logging

logger = logging.getLogger(__name__)

# 1. Verify what a config will do before you run it.
flow = AgentFlow(run_on="docker", steps=[Agent(name="w")])
print(repr(flow))  # confirm 'a Docker container (shared)' before starting

# 2. Log where things run at startup — support tickets get 10x easier to triage.
agent = Agent(name="builder")
logger.info(agent.where_does_it_run())

# 3. Assert where things run in a test.
from praisonaiagents.agent.execution_location import say_place
assert say_place("docker") == "a Docker container"

Compare local, shared-sandbox, and hosted from one script:

python
from praisonaiagents import Agent, AgentFlow
from praisonai import HostedAgent

local = Agent(name="local")
shared = AgentFlow(run_on="docker", steps=[])
hosted = Agent(name="hosted", backend=HostedAgent(provider="anthropic"))

for obj in (local, shared, hosted):
    print(obj.where_does_it_run())
    print("---")

Best Practices

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.

Print a startup banner when running untrusted code

Call where_does_it_run() at startup so a reader sees, in plain English, whether anything is actually isolated before untrusted tools run.

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.

Prefer run_on= for shared state, backend= for whole-loop-remote

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.

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

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