> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph L1["This machine"]
        A1[🤖 Agent] --> T1[🔧 tools]
    end
    subgraph L2["A shared sandbox"]
        A2[🤖 Agent] --> T2[🐳 tools]
    end
    subgraph L3["A provider's cloud"]
        A3[🤖 Agent] --> T3[☁️ tools]
    end
    subgraph L4["A self-hosted container"]
        A4[🐳 Agent] --> T4[🐳 tools]
    end

    B1["thinks_on: this machine<br/>tools_run_on: this machine"]
    B2["thinks_on: this machine<br/>tools_run_on: a Docker container (shared)"]
    B3["thinks_on: Anthropic's cloud<br/>tools_run_on: Anthropic's cloud"]
    B4["thinks_on: a Docker container<br/>tools_run_on: a Docker container"]

    L1 --> B1
    L2 --> B2
    L3 --> B3
    L4 --> B4

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff

    class A1,A2,A3,A4 agent
    class T1,T2,T3,T4 tool
    class B1,B2,B3,B4 answer
```

## Quick Start

<Steps>
  <Step title="Ask an agent where it runs">
    A plain agent thinks and runs its tools on this machine:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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.
    ```
  </Step>

  <Step title="A workflow with a shared sandbox">
    Set `run_on="docker"` and every step shares one sandbox:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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.
    ```
  </Step>

  <Step title="Ask a hosted agent where it runs">
    A hosted backend moves the whole turn to the provider's cloud:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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.
    ```
  </Step>

  <Step title="Read the answer as a dictionary">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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)'}
    ```
  </Step>
</Steps>

***

## 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.

| Topology | Example | `thinks_on` | `tools_run_on` |
| - | - | - | - |
| **Tools remote, loop local** | `Agent(tools_run_on="docker")` | `this machine` | `a Docker container` |
| **Whole loop hosted** | `Agent(run_on="anthropic")` | `Anthropic's cloud` | `Anthropic's cloud` |
| **Whole loop self-hosted** | `Agent(run_on="docker")` | `a Docker container` | `a Docker container` |

<Note>
  `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.
</Note>

<Note>
  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`:

  ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonaiagents.agent.execution_location import say_place

  say_place("anthropic")   # "Anthropic's cloud"
  say_place("docker")      # "a Docker container"
  ```
</Note>

***

## 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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Object as Agent / AgentFlow
    participant Fields as repr_fields()
    participant Describe as describe()
    participant Explain as explain()

    User->>Object: repr(obj)
    Object->>Fields: repr_fields(obj)
    Fields->>Describe: describe(obj)
    Describe->>Object: read backend, run_on, sandbox_config
    Describe-->>Fields: {thinks_on, tools_run_on, code_runs_on?}
    Fields-->>Object: key='value' pairs
    Object-->>User: Agent(name='builder', thinks_on='...', tools_run_on='...')

    User->>Object: where_does_it_run()
    Object->>Describe: read backend, run_on, sandbox
    Describe-->>Object: {thinks_on, tools_run_on}
    Object->>Explain: build sentences
    Explain-->>User: plain-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`](#the-two-flavors-of-local).

<Note>
  As of [PR #4070](https://github.com/MervinPraison/PraisonAI/pull/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.
</Note>

<Note>
  As of [PR #4092](https://github.com/MervinPraison/PraisonAI/pull/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).
</Note>

***

## What each place name means

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

| Provider | Phrase |
| - | - |
| `local` | this machine |
| `subprocess` | a separate process on this machine |
| `sandlock` | a locked-down process on this machine |
| `docker` | a Docker container |
| `e2b` | an E2B cloud sandbox |
| `modal` | a Modal cloud sandbox |
| `daytona` | a Daytona cloud sandbox |
| `flyio` | a Fly.io machine |
| `tenki` | a Tenki cloud sandbox |
| `novita` | a Novita cloud sandbox |
| `ssh` | a remote machine over SSH |
| `anthropic` | Anthropic's cloud |

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

| You type | It reports |
| - | - |
| `native` | a 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"`.

<Note>
  As of [PR #4070](https://github.com/MervinPraison/PraisonAI/pull/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'`.
</Note>

<Note>
  **`native` is a real boundary, consistent with `sandlock` ([PR #4107](https://github.com/MervinPraison/PraisonAI/pull/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`.
</Note>

### 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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 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)
```

<Note>
  **Behaviour-change note ([PR #4070](https://github.com/MervinPraison/PraisonAI/pull/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.
</Note>

***

## The subprocess warning

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

<Warning>
  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.
</Warning>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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](/docs/features/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:

| Field | When it appears | Meaning |
| - | - | - |
| `thinks_on` | always | Where the AI model calls happen |
| `tools_run_on` | always | Where the tools execute |
| `code_runs_on` | when a `sandbox_config` exists | Where `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:

| Method | Returns | When to use |
| - | - | - |
| `repr(obj)` | one line, `key=value` pairs | quick REPL scan |
| `obj.where_does_it_run()` | plain-English sentences | when you want a colleague-friendly explanation |
| `describe(obj)` (advanced) | `{thinks_on, tools_run_on, code_runs_on?}` dict | when you need structured values for a UI/log |

| Symbol | Kind | Returns | Notes |
| - | - | - | - |
| `Agent.where_does_it_run()` | method | `str` | Sentences with the "Nothing is isolated" tail for a fully-local agent |
| `AgentTeam.where_does_it_run()` | method | `str` | Adds "Every step shares that same sandbox…" when `run_on=` is set |
| `AgentFlow.where_does_it_run()` | method | `str` | Same shared-sandbox semantics as `AgentTeam` |
| `repr(Agent \| AgentTeam \| AgentFlow)` | dunder | `str` | Includes `name=`, `agents=`/`steps=` (team/flow), `thinks_on=`, `tools_run_on=` |
| `describe(obj, *, shared=False)` | function | `Dict[str, str]` | Structured form of the answer — never raises |
| `explain(obj, *, shared=False)` | function | `str` | Sentence form — same output as `.where_does_it_run()` |
| `say_place(name)` | function | `str` | Provider name → human phrase (see the mapping table) |
| `repr_fields(obj, *, shared=False)` | function | `str` | `key='value'` pairs used inside custom `__repr__`s |

The helper functions live under `praisonaiagents.agent.execution_location`:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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 theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](https://github.com/MervinPraison/PraisonAI/pull/4107), `native` (an alias for `sandlock`) is correctly excluded, so `tools_run_on="native"` and `tools_run_on="sandlock"` give the same advice.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](https://github.com/MervinPraison/PraisonAI/pull/4107)).
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Sandbox" icon="shield-halved" href="/docs/features/sandbox">
    Per-agent `sandbox=` and the explicit `execute_code()` API
  </Card>

  <Card title="Shared Sandbox" icon="share-nodes" href="/docs/features/shared-sandbox">
    `run_on=` gives a whole flow one shared sandbox
  </Card>

  <Card title="Sandboxed Agent" icon="sandbox" href="/docs/features/sandboxed-agent">
    Local loop, sandboxed tools
  </Card>

  <Card title="Self-Hosted Agent (Docker)" icon="docker" href="/docs/features/run-on-docker">
    `run_on="docker"` — the whole loop self-hosted
  </Card>

  <Card title="Hosted Agent" icon="cloud" href="/docs/features/hosted-agent">
    Run the whole agent loop on a provider's cloud
  </Card>

  <Card title="Local Agent" icon="desktop" href="/docs/features/local-agent">
    Run agent loops locally with any LLM
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.