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

# Placement — where does my agent run?

> One vocabulary for where the whole workflow, an agent's tools, and its explicit code calls run

Placement decides where work happens: a whole flow, one agent's tools, and its explicit code calls each answer a different question with a different parameter.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, AgentFlow

writer = Agent(name="writer", instructions="Write /workspace/data.txt")
reader = Agent(name="reader", instructions="Read /workspace/data.txt")

# Every step's tools share one Docker sandbox; thinking stays here
flow = AgentFlow(run_on="docker", steps=[writer, reader])

print(flow.where_does_it_run())
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Flow[🔀 AgentFlow / AgentTeam] -->|run_on=| Shared[📦 One shared sandbox]
    Local[🤖 LocalAgent] -->|compute=| Provider[☁️ Compute provider]
    Agent[🤖 Agent] -->|sandbox=| Explicit[⚙️ execute_code sandbox]
    Shared -.thinking stays.-> Here1[💻 This machine]
    Provider -.thinking stays.-> Here1

    classDef flow fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef sandbox fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef here fill:#6366F1,stroke:#7C90A0,color:#fff

    class Flow,Local,Agent flow
    class Shared,Provider,Explicit sandbox
    class Here1 here
```

## Quick Start

<Steps>
  <Step title="Share one sandbox across a flow or team">
    Thinking stays on this machine; every step's shell, file and code tools run in one shared Docker sandbox.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent, AgentFlow

    writer = Agent(name="writer", instructions="Write /workspace/data.txt")
    reader = Agent(name="reader", instructions="Read /workspace/data.txt")

    flow = AgentFlow(run_on="docker", steps=[writer, reader])
    ```
  </Step>

  <Step title="Move one agent's tools">
    Give a single `LocalAgent` its own remote compute provider — thinking stays local, tools run on the provider.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai import LocalAgent

    agent = LocalAgent(compute="docker")
    ```
  </Step>

  <Step title="Isolate explicit code calls">
    `Agent(sandbox=…)` configures the explicit `agent.execute_code()` API. It does not add tools the model can call.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(name="runner", instructions="Run code", sandbox=True)
    agent.execute_code_sync("print(6*7)")
    ```
  </Step>
</Steps>

***

## How It Works

Each parameter moves a different scope, so the choices never overlap.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Flow
    participant LLM
    participant Sandbox

    User->>Flow: flow.run(request)
    Flow->>LLM: think (on this machine)
    LLM-->>Flow: call a tool
    Flow->>Sandbox: run shell / file tool (run_on=)
    Sandbox-->>Flow: result
    Flow-->>User: response
```

| Parameter  | What it moves                                                     | What it accepts               | Where it lives                                     |
| ---------- | ----------------------------------------------------------------- | ----------------------------- | -------------------------------------------------- |
| `run_on=`  | **every step's tools** in a flow or team, into ONE shared sandbox | compute providers (see below) | `AgentFlow`, `AgentTeam`, YAML top-level `run_on:` |
| `compute=` | **one agent's tools**; thinking stays local                       | compute providers             | `LocalAgent`, `LocalManagedAgent`                  |
| `sandbox=` | the explicit `agent.execute_code()` calls                         | `True` / `SandboxConfig`      | `Agent(...)`                                       |

***

## Which parameter do I want?

The scope you want to move picks the parameter. On a single `Agent`, `run_on=` hands the **whole** loop to any hosted place — see the [eight-place table below](#where-run-on-and-compute-can-point).

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What do you want<br/>to move?}
    Q -->|The whole agent loop| R[run_on=<br/>any hosted place]
    R --> RT[anthropic · docker · e2b · modal<br/>daytona · flyio · tenki · novita]
    Q -->|Every step in a flow/team| A[run_on=<br/>shared sandbox]
    Q -->|One agent's tools| B[compute=<br/>compute provider]
    Q -->|Explicit execute_code calls| C[sandbox=<br/>SandboxConfig]

    classDef question fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef answer fill:#10B981,stroke:#7C90A0,color:#fff

    class Q question
    class A,B,C,R,RT answer
```

<Note>
  **Behaviour-change note ([PR #4071](https://github.com/MervinPraison/PraisonAI/pull/4071)).** `run_on=` on `Agent`, `AgentFlow`, and `AgentTeam` now accepts every hosted place — `anthropic`, `docker`, `e2b`, `modal`, `daytona`, `flyio`, `tenki`, `novita` — not just `anthropic` and `docker`. One generic backend wraps any compute provider, so each place can host the whole loop. `local`, `ssh`, `subprocess`, and `sandlock` are refused for `run_on=` (they isolate tools or need a host object; they are not runtimes for a loop). The Modal path (`run_on="modal"` and `execute_code(run_in="modal")`) is also fixed and verified end to end.

  The same widening is now covered by tests for the underlying `HostedAgent(provider=...)` factory as of [PR #4074](https://github.com/MervinPraison/PraisonAI/pull/4074) — `run_on="daytona"` is exactly `backend=HostedAgent(provider="daytona")`, and both are now regression-tested end to end. See [HostedAgent Compute Runtimes](/docs/features/hosted-agent-compute-runtimes).

  [PR #4077](https://github.com/MervinPraison/PraisonAI/pull/4077) completes the picture — `docker` now goes through the same generic backend as the rest, so `praisonai managed ps` / `managed stop` reclaim Docker containers reliably (previously they could be listed but not stopped). [PR #4107](https://github.com/MervinPraison/PraisonAI/pull/4107) finishes the job by making `DockerCompute` inherit `SyncComputeProvider` — the previous extraction missed four sites (`provision`, `shutdown`, `execute`, `list_instances`) that still called `run_in_executor` directly and shared the same shutdown leak. See [Reclaim Stray Sandboxes](/docs/features/reclaim-stray-sandboxes#behaviour-change-the-silent-leak-on-shutdown). [PR #4109](https://github.com/MervinPraison/PraisonAI/pull/4109) closes two remaining leaks in the same story: the timed-out docker `execute()` container is now killed by name, and `ComputeManagedAgent` finally registers a `weakref.finalize` so `run_on=` instances are reclaimed when the backend is collected or the process exits. See [the `run_on=` finalizer](/docs/features/reclaim-stray-sandboxes#and-the-run-on-finalizer-that-was-never-registered-pr-4109).

  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>

<Note>
  `docker` legitimately appears under both `run_on=` and `tools_run_on=` — same place, two scopes, carried by the parameter. `run_on="docker"` runs the whole loop in the container; `tools_run_on="docker"` keeps the loop local and moves only the tools. See [Self-Hosted Agent (Docker)](/docs/features/run-on-docker).

  `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>
  `tools_run_on=` also accepts the aliases the resolver understands (`native` → `sandlock`, and any spelling in `_compute_bridge._ALIASES`). Since [PR #4070](https://github.com/MervinPraison/PraisonAI/pull/4070) these are validated at construction, so `Agent(tools_run_on="native")` no longer raises `TypeError`.
</Note>

***

## Where `run_on=` and `compute=` can point

`run_on=` and `compute=` accept the same **eight** compute providers. As of [PR #4071](https://github.com/MervinPraison/PraisonAI/pull/4071), `run_on=` hosts the whole agent loop on each of them (previously only `anthropic` and `docker`), and all eight are valid for both `run_on=` and `tools_run_on=`.

| Provider    | Description                   | `run_on=` hosts the loop |
| ----------- | ----------------------------- | ------------------------ |
| `anthropic` | Vendor-hosted managed runtime | ✅                        |
| `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 microVM         | ✅                        |
| `novita`    | A Novita cloud sandbox        | ✅                        |

A typo is rejected at load / call time with the full list of valid names.

`run_on=` refuses the places that isolate tools or need a host object — they are not runtimes for a loop:

| Place                     | Why `run_on=` refuses it                                    |
| ------------------------- | ----------------------------------------------------------- |
| `local`                   | Would just run in your own shell — same as passing nothing  |
| `ssh`                     | Needs a host object (`SSHSandbox(host=…)`), not a bare name |
| `subprocess` / `sandlock` | Isolate tools; aren't runtimes for a loop                   |

`tools_run_on=` reaches additional local backends and aliases the resolver understands — every alias in `_compute_bridge._ALIASES` is accepted at construction as of [PR #4070](https://github.com/MervinPraison/PraisonAI/pull/4070):

| Provider     | Description                                                                                                                      |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `subprocess` | A separate process on this machine (scrubbed env; **not** a security boundary)                                                   |
| `sandlock`   | A locked-down process on this machine (Landlock + seccomp-bpf)                                                                   |
| `native`     | Alias — resolves to `sandlock` (accepted at construction since [PR #4070](https://github.com/MervinPraison/PraisonAI/pull/4070)) |
| `ssh`        | A remote machine over SSH                                                                                                        |

The explicit `Agent(sandbox=…)` API accepts additional local sandbox backends — `subprocess`, `sandlock`, `docker` — through `SandboxConfig`. See [Sandbox Backends](/docs/features/sandbox-backends).

<Warning>
  `local` / `subprocess` is **not** a security boundary. Its blocked-command list is bypassable by ordinary shell syntax (`cat $(echo /etc/passwd)` reads the file). For untrusted code, use `docker`, a cloud provider, or `sandlock` (via `Agent(sandbox=…)`).
</Warning>

***

## Bringing your own compute place

A compute backend can ship in its own package and register under the `praisonai.compute` entry-point group — no change to the main repo. Once installed, its name works with `run_on=`, `compute=`, and `tools_run_on=` like a built-in.

```toml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# pyproject.toml — in the plugin package
[project.entry-points."praisonai.compute"]
runpod = "praisonai_compute_runpod:RunpodCompute"
```

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

# 'runpod' resolves through the entry-point registry
agent = Agent(name="runner", instructions="Run code", tools_run_on="runpod")
```

| Behaviour       | What happens                                                                                                                                                      |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Discovery       | Providers load on demand from the `praisonai.compute` entry-point group — a broken plugin fails only for the caller that names it.                                |
| Friendly phrase | A provider that sets `display_name = "a Runpod cloud sandbox"` on its class uses that phrase in `where_does_it_run()` and `repr(agent)` instead of the bare name. |
| `managed ps`    | Derives its list from the registry, so contributed places are listed and stopped automatically — no extra wiring.                                                 |

See [Compute Provider Plugins](/docs/features/compute-provider-plugins) for the full protocol and a minimal example.

***

## Inspect placement

`where_does_it_run()` prints the answer in plain English. It is available on `Agent`, `AgentFlow` and `AgentTeam`, and reports two fields — `thinks_on` (the model calls) and `tools_run_on` (the tools).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, AgentFlow

writer = Agent(name="writer", instructions="Write")
reader = Agent(name="reader", instructions="Read")

flow = AgentFlow(run_on="docker", steps=[writer, reader])
print(flow.where_does_it_run())
```

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

A bare local agent reports that nothing is isolated:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
print(Agent(name="builder", instructions="Build").where_does_it_run())
```

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

<Note>
  `thinks_on` and `tools_run_on` are **output fields** from `where_does_it_run()` — they describe where work lands. They are not constructor parameters. Set placement with `run_on=`, `compute=`, or `sandbox=`.
</Note>

<Note>
  **Behaviour-change note ([PR #4070](https://github.com/MervinPraison/PraisonAI/pull/4070)).** `repr(agent)` and `where_does_it_run()` now report a managed backend's `LocalCompute` as *"a plain shell on this machine (no policy applied)"* rather than *"a separate process on this machine"*. The runtime behaviour did not change — the previous string understated the shell's freedom and overstated the sandbox's protection. See [Where It Runs](/docs/features/where-does-it-run#the-two-flavors-of-local).
</Note>

***

## Migration

<AccordionGroup>
  <Accordion title="Placement parameters at a glance">
    | Scope                                  | Parameter                                     | Where                                    |
    | -------------------------------------- | --------------------------------------------- | ---------------------------------------- |
    | Whole flow / team → one shared sandbox | `run_on="docker"`                             | `AgentFlow`, `AgentTeam`, YAML `run_on:` |
    | One agent's tools → a compute provider | `compute="docker"`                            | `LocalAgent`, `LocalManagedAgent`        |
    | Explicit `execute_code()` calls        | `sandbox=True` / `sandbox=SandboxConfig(...)` | `Agent`                                  |

    YAML `run_on:` maps directly to the Python `AgentFlow(run_on=…)` kwarg — same name, same providers, validated at load time.
  </Accordion>

  <Accordion title="ExecutionConfig.code_sandbox_mode is inert">
    `ExecutionConfig(code_sandbox_mode="sandbox")` still exists with a `"sandbox"` default and survives `to_dict()` / `from_dict()`, but **no execution path reads it** today. It does not isolate anything on its own. To isolate code the model runs, use `AgentFlow(run_on="docker")` or `Agent(sandbox=…)`. See [Execution Systems](/docs/features/execution-systems).
  </Accordion>
</AccordionGroup>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Use sandlock for a real local boundary">
    `subprocess` separates a process but does not contain it. `sandlock` enforces Landlock + seccomp-bpf, the only kernel-level boundary that runs on this machine. Configure it through `Agent(sandbox=SandboxConfig.native())`.
  </Accordion>

  <Accordion title="Share one sandbox with run_on= when steps hand off files">
    `run_on=` on a flow or team provisions one sandbox for the whole run, so a file written by one step is visible to the next. Leave it unset for zero-overhead local execution.
  </Accordion>

  <Accordion title="sandbox= isolates only explicit execute_code() calls">
    `Agent(sandbox=…)` does not add a model-visible tool and does not protect the host `execute_command` path. For model-driven isolation, run the whole workflow in a real container with `AgentFlow(run_on="docker")`.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Self-Hosted Agent (Docker)" icon="docker" href="/docs/features/run-on-docker">
    `run_on="docker"` runs the whole agent in a container you own.
  </Card>

  <Card title="Shared Sandbox" icon="box" href="/docs/features/shared-sandbox">
    Run every step's tools in one shared sandbox.
  </Card>

  <Card title="Sandbox" icon="shield" href="/docs/features/sandbox">
    Run tools and code in an isolated environment.
  </Card>

  <Card title="Sandbox Guarantees" icon="lock" href="/docs/features/sandbox-guarantees">
    What each boundary does and does not protect.
  </Card>
</CardGroup>
