Access Control

Four independent controls decide what a running route can do: tool scoping, permissions, the execution sandbox and guarded delegation. Each one answers a different question, so they compose. A route uses the controls it needs.

ControlQuestion it answersDocs
Tool scopingWhich tools can the model call?Tools
PermissionsWhether a given call runs (human-in-the-loop approval)Permissions
Execution sandboxWhat an allowed, approved call can actually touchExecution Sandbox
Guarded delegationWhether a child receives the delegated input before any of its tools executeSubagents

Tool scopingCopy link to section: Tool scoping

agent({ tools: { allow, deny, approve, constrain } }) controls the surface offered to the model:

  • deny revokes a tool so it's never wired into the generated entry.
  • allow grants back a capability tool withheld from a subagent, such as readFile or runBash.
  • deny wins when a name appears in both.
  • Denying writeFile also withholds editFile, unless allow names editFile. An allow that names writeFile doesn't grant editFile.

B4.run enforces this at composition time, so the model has no way to call a denied tool. Here's a route that removes runBash:

src/app/ops/index.ts
export default agent({ model: "gpt-5", systemPrompt: "…", tools: { deny: ["runBash"] } })

Scoping controls the surface. Once a tool is granted, it does whatever its implementation permits. A granted writeFile can still write anywhere it's allowed to. See Tools.

PermissionsCopy link to section: Permissions

Two gates are on by default for the built-in workspace tools. runBash commands are matched against allow/deny patterns, and filesystem paths outside workspace/ are permission-gated. An unmatched ("unknown") call pauses the run and asks a human, unless permissions.mode is set to non-interactive (fail-closed) or bypass (dev/test only).

Two more gates ride the same interrupt machinery. tools: { approve: [...] } requires human approval before a named tool call, and memory: { writes: "ask" } gates belief-contradiction memory writes. Use tools.approve for a separately authored tool such as deployProd. The workspace command and path tools already have their pattern-aware gates. See Permissions for the interrupt payloads and resume flow.

Execution sandboxCopy link to section: Execution sandbox

Even a tool that's allowed and approved still runs somewhere. By default that's the local workspace/ directory on the host. Add a sandbox key to b4.config.ts and every readFile, writeFile, editFile, listDir and runBash call for a thread runs in an isolated environment: filesystem, shell and (optionally) network. B4.run ships a Docker reference provider and a Kubernetes provider behind the same SandboxProvider interface. See Execution Sandbox for what any provider guarantees.

Guarded delegationCopy link to section: Guarded delegation

A parent's delegation policy runs at the last step of dispatch, before the child starts. Its allow, deny, approve or constraint decision determines whether the child receives the delegated input at all. The child's own tool policies and permission gates run only after that decision allows dispatch. See Subagents.

How they composeCopy link to section: How they compose

A route that uses pattern-gated shell commands in an isolated environment, separately approves an authored deployment tool, and reviews delegated input uses all four controls. First, the config:

b4.config.ts
import { config } from "@b4run/cli"
import { dockerSandbox } from "@b4run/sandbox"
 
export default config({
  permissions: {
    allow: { bash: ["ls", "cat"] },
    deny: { bash: ["rm -rf", "sudo"] },
  },
  sandbox: { provider: dockerSandbox({ scope: "my-app", image: "node:24-slim" }) }, // what runBash can touch
})

Then the route:

src/app/ops/index.ts
import { agent } from "@b4run/sdk"
import researcher from "./subagents/researcher/index.js"
 
export default agent({
  model: "gpt-5",
  systemPrompt: "…",
  // Denying writeFile also withholds editFile, so this route can't write files.
  tools: { deny: ["writeFile"], approve: ["deployProd"] },
  subagents: { researcher },
  delegation: {
    rules: {
      researcher: { action: "approve", reason: "Review delegated customer context." },
    },
  },
})