Each agent in a multi-agent setup can override the global sandbox and tool policy. This page covers per-agent configuration, precedence rules, and examples.
Sandboxing
Backends and modes — full sandbox reference.
Sandbox vs tool policy vs elevated
Debug "why is this blocked?"
Elevated mode
Elevated exec for trusted senders.
Auth is scoped by agent: each agent has its own <agentDir>/apus-agent.sqlite auth store (by default, ~/.apus/agents/<agentId>/agent/apus-agent.sqlite). Never reuse agentDir across agents. Agents can read through to the default/main agent's auth profiles when they do not have a local profile, but OAuth refresh tokens are not cloned into secondary agent stores. If you copy credentials manually, copy only portable static api_key or token profiles.
Configuration examples
Example 1: Personal + restricted family agent
```json
{
"agents": {
"ownership": "explicit",
"defaults": {
"heartbeat": { "agentId": "main" },
"systemAgent": { "agentId": "main" }
},
"entries": {
"main": {
"name": "Personal Assistant",
"workspace": "~/.apus/workspace",
"sandbox": { "mode": "off" }
},
"family": {
"name": "Family Bot",
"workspace": "~/.apus/workspace-family",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read", "message"],
"deny": ["exec", "write", "edit", "apply_patch", "process", "browser"],
"message": {
"crossContext": {
"allowWithinProvider": false,
"allowAcrossProviders": false
}
}
}
}
}
},
"bindings": [
{
"agentId": "family",
"match": {
"channel": "whatsapp",
"accountId": "*",
"peer": {
"kind": "group",
"id": "120363424282127706@g.us"
}
}
},
{
"agentId": "main",
"match": { "channel": "whatsapp", "accountId": "*" }
}
],
"talk": { "agentId": "main" }
}
```
**Result:**
- `main` agent: runs on host, full tool access.
- `family` agent: runs in the configured container sandbox backend (one container per agent), only `read` and current-conversation message sends.
Example 2: Work agent with shared sandbox
```json
{
"agents": {
"ownership": "explicit",
"defaults": {
"heartbeat": { "agentId": "personal" },
"systemAgent": { "agentId": "personal" },
"authInheritance": { "agentId": "personal" }
},
"entries": {
"personal": {
"workspace": "~/.apus/workspace-personal",
"sandbox": { "mode": "off" }
},
"work": {
"workspace": "~/.apus/workspace-work",
"sandbox": {
"mode": "all",
"scope": "shared",
"workspaceRoot": "/tmp/work-sandboxes"
},
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": ["browser", "gateway", "discord"]
}
}
}
},
"talk": { "agentId": "personal" }
}
```
Example 2b: Global coding profile + messaging-only agent
```json
{
"tools": { "profile": "coding" },
"agents": {
"ownership": "explicit",
"defaults": {
"heartbeat": { "agentId": "main" },
"systemAgent": { "agentId": "main" }
},
"entries": {
"main": {
"workspace": "~/.apus/workspace"
},
"support": {
"tools": { "profile": "messaging", "allow": ["slack"] }
}
}
},
"talk": { "agentId": "main" }
}
```
**Result:**
- `main` inherits the global coding tool profile.
- `support` agent is messaging-only (+ Slack tool).
Example 3: Different sandbox modes per agent
```json
{
"agents": {
"ownership": "explicit",
"defaults": {
"heartbeat": { "agentId": "main" },
"systemAgent": { "agentId": "main" },
"sandbox": {
"mode": "non-main",
"scope": "session"
}
},
"entries": {
"main": {
"workspace": "~/.apus/workspace",
"sandbox": {
"mode": "off"
}
},
"public": {
"workspace": "~/.apus/workspace-public",
"sandbox": {
"mode": "all",
"scope": "agent"
},
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch"]
}
}
}
},
"talk": { "agentId": "main" }
}
```
Configuration precedence
When both global (agents.defaults.*) and agent-specific (agents.entries.*.*) configs exist:
Sandbox config
Agent-specific settings override global:
agents.entries.*.sandbox.mode > agents.defaults.sandbox.mode
agents.entries.*.sandbox.scope > agents.defaults.sandbox.scope
agents.entries.*.sandbox.workspaceRoot > agents.defaults.sandbox.workspaceRoot
agents.entries.*.sandbox.workspaceAccess > agents.defaults.sandbox.workspaceAccess
agents.entries.*.sandbox.docker.* > agents.defaults.sandbox.docker.*
agents.entries.*.sandbox.browser.* > agents.defaults.sandbox.browser.*
agents.entries.*.sandbox.prune.* > agents.defaults.sandbox.prune.*
agents.entries.*.sandbox.{docker,browser,prune}.* overrides agents.defaults.sandbox.{docker,browser,prune}.* for that agent (ignored when sandbox scope resolves to "shared"). The docker block configures both built-in container backends.
Tool restrictions
The filtering order is:
Tool profile
`tools.profile` or `agents.entries.*.tools.profile`.
Provider tool profile
`tools.byProvider[provider].profile` or `agents.entries.*.tools.byProvider[provider].profile`.
Global tool policy
`tools.allow` / `tools.deny`.
Provider tool policy
`tools.byProvider[provider].allow/deny`.
Agent-specific tool policy
`agents.entries.*.tools.allow/deny`.
Agent provider policy
`agents.entries.*.tools.byProvider[provider].allow/deny`.
Sandbox tool policy
`tools.sandbox.tools` or `agents.entries.*.tools.sandbox.tools`.
Subagent tool policy
`tools.subagents.tools`, if applicable.
Precedence rules
- Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
- If `agents.entries.*.tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
- If `agents.entries.*.tools.profile` is set, it overrides `tools.profile` for that agent.
- Provider tool keys accept either `provider` (e.g. `anthropic`) or `provider/model` (e.g. `openai/gpt-5.4`).
Empty allowlist behavior
If any explicit allowlist in that chain leaves the run with no callable tools, Apus stops before submitting the prompt to the model. This is intentional: an agent configured with a missing tool such as `agents.entries.*.tools.allow: ["query_db"]` should fail loudly until the plugin that registers `query_db` is enabled, not continue as a text-only agent.
Tool policies support group:* shorthands that expand to multiple tools. See Tool groups for the full list.
Configured MCP tools use the same policy surface. Their canonical names are
<safe-server>__<safe-tool>; globs can target a server namespace. For example:
{
agents: {
entries: {
research: {
tools: {
allow: ["docs__read_docs"],
deny: ["docs__delete_*"],
},
},
},
},
}
Every restrictive layer intersects with the earlier layers, and deny always wins. Apus projects the resulting raw tool set into native Claude, Codex, and Gemini MCP filters before their first model turn. Backend-native names and settings are implementation details, not a second operator policy surface. An MCP server with no allowed tool is omitted. A restrictive catalog failure also omits that server and records a diagnostic instead of failing open.
Per-agent elevated overrides (agents.entries.*.tools.elevated) can further restrict elevated exec for specific agents. See Elevated mode for details.
Migration from single agent
Before (single agent)
```json
{
"agents": {
"defaults": {
"workspace": "~/.apus/workspace",
"sandbox": {
"mode": "non-main"
}
}
},
"tools": {
"sandbox": {
"tools": {
"allow": ["read", "write", "apply_patch", "exec"],
"deny": []
}
}
}
}
```
After (explicit agent)
```json
{
"agents": {
"entries": {
"main": {
"workspace": "~/.apus/workspace",
"sandbox": { "mode": "off" }
}
}
}
}
```
Doctor migrates legacy agents.list rosters and default markers to
agents.entries with explicit surface owners. After replacing the binary
directly, run apus doctor --fix before starting the Gateway. Migrations for
pre-June keys such as sandbox.perSession, embeddedPi, and embeddedHarness
are retired; use sandbox.scope, embeddedAgent, and provider/model runtime
policy. For an older installation,
upgrade through 2026.9.5 before
installing the latest version.
Tool restriction examples
Read-only agent
```json
{
"tools": {
"allow": ["read"],
"deny": ["exec", "write", "edit", "apply_patch", "process"]
}
}
```
Shell execution with filesystem tools disabled
```json
{
"tools": {
"allow": ["read", "exec", "process"],
"deny": ["write", "edit", "apply_patch", "browser", "gateway"]
}
}
```
This policy disables Apus filesystem tools, but `exec` is still a shell and can write files wherever the selected host or sandbox filesystem allows. For a read-only agent, deny `exec` and `process`, or combine shell access with sandbox filesystem controls such as `agents.defaults.sandbox.workspaceAccess: "ro"` or `"none"`.
Communication-only
This complete configuration applies the tool allow/deny policy to the `communication` agent and sets session visibility for every agent on the Gateway:
```json
{
"tools": {
"sessions": { "visibility": "tree" }
},
"agents": {
"entries": {
"communication": {
"tools": {
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
}
}
}
}
}
```
`tools.sessions.visibility` is Gateway-wide and cannot be set per agent. Session tools default to `all` with agent-to-agent messaging on. With `tree`, callers can access their current session and sessions they spawn; the canonical main session can still access every session belonging to its agent. Incognito restrictions and the sandbox spawned-session clamp still apply. See [`tools.sessions`](/gateway/config-tools#tools-sessions) and [`tools.agentToAgent`](/gateway/config-tools#tools-agenttoagent).
`sessions_history` in this profile still returns a bounded, sanitized recall view rather than a raw transcript dump. Assistant recall strips thinking tags, `<relevant-memories>` scaffolding, plain-text tool-call XML payloads (including `<tool_call>...</tool_call>`, `<function_call>...</function_call>`, `<tool_calls>...</tool_calls>`, `<function_calls>...</function_calls>`, and truncated tool-call blocks), downgraded tool-call scaffolding, leaked ASCII/full-width model control tokens, and malformed MiniMax tool-call XML before redaction/truncation.
Common pitfall: "non-main"
agents.defaults.sandbox.mode: "non-main" checks the session key against the main session key (always "main"; session.mainKey is not user-configurable, and Apus warns and ignores any other value), not the agent id. Group/channel sessions always get their own keys, so they are treated as non-main and will be sandboxed. If you want an agent to never sandbox, set agents.entries.*.sandbox.mode: "off".
Testing
After configuring multi-agent sandbox and tools:
Check agent resolution
```bash
apus agents list --bindings
```
Verify sandbox containers
```bash
docker ps --filter "name=apus-sbx-"
```
Test tool restrictions
- Send a message requiring restricted tools.
- Verify the agent cannot use denied tools.
Monitor logs
```bash
apus logs --follow | grep -E "routing|sandbox|tools"
```
Troubleshooting
Tools still available despite deny list
- Check the [full filtering order](#tool-restrictions): profile → provider profile → global policy → provider policy → agent policy → agent provider policy → sandbox → subagent.
- Each level can only further restrict, not grant back.
- See [Sandbox vs tool policy vs elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for step-by-step debugging.
- For MCP tools, use the provider-safe name shown by Apus, such as `docs__read_docs` or `docs__*`; do not use a backend's raw config field name.
Container not isolated per agent
- Default `scope` is `"agent"` (one container per agent id).
- Set `scope: "session"` for one container per session, or `scope: "shared"` to reuse one container across agents.
Related
- Elevated mode
- Multi-agent routing
- Sandbox configuration
- Sandbox vs tool policy vs elevated — debugging "why is this blocked?"
- Sandboxing — full sandbox reference (modes, scopes, backends, images)
- Session management
- OpenShell — a managed sandbox backend a per-agent sandbox can delegate to
- ACP agents — a separate boundary: Apus sandbox policy does not wrap ACP harness execution
- Sub-agents — the spawned sessions these limits clamp