Control plane tools

Two built-in tools remain control-plane sensitive:

The gateway tool stays owner-only because config reads can expose secrets and host topology and update.run changes the running installation. Updates require an explicit user request. Agents request other persistent config or lifecycle changes through the apus delegation tool. Apus maps them to typed operations and applies the requesting run's effective permission policy: Full Access, including Default (Full Access), authorizes permitted changes without an approval prompt; restricted runs require human approval. Independent tool, filesystem, sandbox, and operation restrictions still apply, and the host revalidates live authority before execution. See Session permission modes and Apus setup agent.

For any agent/surface handling untrusted content, deny these by default:

{
  tools: {
    deny: ["gateway", "cron", "sessions_spawn", "sessions_send"],
  },
}

commands.restart=false disables /restart and external SIGUSR2 restart requests. The gateway agent tool has no restart action.

Cross-provider messaging

Agents with message-tool access can send across conversations and channel providers by default. For example, a WebChat session can send to a configured Discord channel without opting in to cross-provider delivery. Tool access, message-action allowlists, channel/account policy, and the provider's own permissions still apply.

tools.message.crossContext.allowAcrossProviders defaults to true. Upgrades adopt this default when the setting is omitted; an explicit false continues to block guarded message actions from crossing the bound source provider. Set it to false if your deployment relies on that restriction.

allowWithinProvider also defaults to true and independently controls actions targeting another conversation on the same provider. To confine guarded message actions to the current bound conversation, set both options to false:

{
  tools: {
    message: {
      crossContext: {
        allowWithinProvider: false,
        allowAcrossProviders: false,
      },
    },
  },
}

To apply this policy to one agent, put the same message block under agents.entries.<agentId>.tools. Per-agent values override the matching global settings; omitted fields inherit them.

These controls guard message mutations such as sends, replies, edits, deletes, and thread creation. They do not restrict every channel action: reads and administrative actions have separate permissions. They also do not confine CLI calls without a bound source conversation or replace filesystem, shell, or sandbox restrictions. See Tool policy and Per-agent access profiles for the related controls.

Node execution (system.run)

If a macOS node is paired, the Gateway can invoke system.run on it - this is remote code execution on that Mac.

Dynamic skills (watcher / remote nodes)

Apus can refresh the skills list mid-session: the skills watcher updates the snapshot on the next agent turn when SKILL.md changes, and connecting a macOS node can make macOS-only skills eligible (based on bin checking). Treat skill folders as trusted code and restrict who can modify them.

Plugins

Plugins run in-process with the Gateway - treat them as trusted code.

Details: Plugins

Sandboxing

Dedicated doc: Sandboxing

Two complementary approaches:

To prevent cross-agent access, keep agents.defaults.sandbox.scope at "agent" (default) or use "session" for stricter per-session isolation. scope: "shared" uses a single container or workspace.

Agent workspace access inside the sandbox (agents.defaults.sandbox.workspaceAccess):

Extra sandbox.docker.binds are validated against normalized, canonicalized source paths. A blocked-path denylist covers /etc, /private/etc, /proc, /sys, /dev, /root, /boot, and directories that commonly contain or alias the Docker socket (/run, /var/run, and docker.sock under them), plus HOME credential subpaths (.aws, .cargo, .config, .docker, .gnupg, .netrc, .npm, .ssh). Parent-symlink tricks and canonical home aliases are resolved through existing ancestors and re-checked, so they still fail closed if they resolve into a blocked root.

tools.elevated is the global baseline escape hatch that runs exec outside the sandbox. The effective host is gateway by default, or node when the exec target is configured to node. Keep tools.elevated.allowFrom tight and do not enable it for strangers. Further restrict per agent via agents.entries.*.tools.elevated. See Elevated mode.

Sub-agent delegation guardrail

If you allow session tools, treat delegated sub-agent runs as another boundary decision:

Read-only mode

Build a read-only profile by combining agents.defaults.sandbox.workspaceAccess: "ro" (or "none" for no workspace access) with tool allow/deny lists that block write, edit, apply_patch, exec, process, etc.

Per-agent access profiles (multi-agent)

Each agent can have its own sandbox + tool policy: full access, read-only, or no access. See Multi-Agent Sandbox & Tools for precedence rules.

Common patterns: personal agent (full access, no sandbox), family/work agent (sandboxed + read-only tools), public agent (sandboxed + no filesystem/shell tools).

Tool profiles do not narrow session-tool reach, and sandboxing only clamps the sandboxed caller to its spawn tree; an unsandboxed agent can still read a sandboxed agent's sessions. Session visibility is Gateway-wide and agent-to-agent messaging is on by default, so pair persona profiles with tools.sessions.visibility and tools.agentToAgent when agents on one Gateway should not see or message each other (see the last example below).

To allow requests without cross-agent history access, keep visibility narrow and set the requester's agents.entries.<agentId>.tools.agentToAgent.send to selected target IDs or * patterns. This grants only sessions_send and the authorized turn's reply, not read, status, session-control, or otherwise hidden watch access. It does not override global agent-to-agent restrictions or sandbox spawned-only clamps. See the send-only example and trust limits.

Full access (no sandbox)

{
  agents: {
    entries: {
      personal: {
        workspace: "~/.apus/workspace-personal",
        sandbox: { mode: "off" },
      },
    },
  },
}

Read-only tools + read-only workspace

{
  agents: {
    entries: {
      family: {
        workspace: "~/.apus/workspace-family",
        sandbox: { mode: "all", scope: "agent", workspaceAccess: "ro" },
        tools: {
          allow: ["read"],
          deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
        },
      },
    },
  },
}

No filesystem/shell access (provider messaging allowed)

{
  // The default "all" covers every session on the Gateway, including other agents' and other users' transcripts.
  // Explicit tree scope limits non-main callers to current + spawned sessions.
  // Use visibility: "self" for strict current-session access, including main.
  tools: { sessions: { visibility: "tree" } }, // self | tree | agent | all
  agents: {
    entries: {
      public: {
        workspace: "~/.apus/workspace-public",
        sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
        tools: {
          allow: [
            "sessions_list",
            "sessions_history",
            "sessions_send",
            "sessions_spawn",
            "session_status",
            "discord",
            "slack",
            "telegram",
            "whatsapp",
          ],
          deny: [
            "apply_patch",
            "browser",
            "canvas",
            "cron",
            "edit",
            "exec",
            "gateway",
            "image",
            "nodes",
            "process",
            "read",
            "write",
          ],
        },
      },
    },
  },
}