Security Architecture

B4.run's route middleware is an execution hook, and it only sees requests that are about to run a route. So put outer authentication and tenant authorization in front of every non-local runtime endpoint. Then layer B4.run's inner agent controls behind it.

Pages in this sectionCopy link to section: Pages in this section

This page covers the service edge. The other Secure pages cover the checks inside B4.run:

PageWhat it covers
Thread AccessWho may create, read, change or delete a thread, checked in the runtime on every thread endpoint.
Access ControlThe four controls on a running route (tool scoping, permissions, execution sandbox and guarded delegation) and how they compose.
PermissionsHuman-in-the-loop approval for commands, paths, tools, subagents and memory writes.
Execution SandboxRunning the workspace tools in an isolated filesystem, shell and network.
Kubernetes SandboxThe Pod-backed sandbox provider and the cluster setup it needs.

Start at the service edgeCopy link to section: Start at the service edge

Authenticate the caller before the request reaches the B4.run service, and restrict network reachability where you can. The outer layer should:

  • reject unauthenticated access on every route,
  • apply rate and body-size limits,
  • terminate TLS,
  • attach verified identity for authorization and audit.

This layer must cover the entire service. Management and health routes never reach src/middleware.ts, so protect them here.

Endpoint coverageCopy link to section: Endpoint coverage

B4.run middleware runs when a request is about to execute a route, and on the two reads that disclose a parked prompt's payload. Other management surfaces bypass it.

SurfaceExamplesB4.run middlewareOuter auth required
HealthGET /healthzNoYes on a non-local service. A platform may expose a separately constrained probe path.
Thread managementPOST /threads, GET /threads/:thread_id, DELETE /threads/:thread_idNoYes, with per-thread ownership checks
StateGET /threads/:thread_id/stateNoYes, with per-thread ownership checks
Parked prompts and turnsGET /threads/:thread_id/pending_interrupts, GET /threads/:thread_id/turns, GET /threads/:thread_id/eventsYes, the parking route's middlewareYes, with per-thread ownership checks
CancellationPOST /threads/:thread_id/cancelNoYes. It controls process-local execution.
Agent Protocol execution/threads/:thread_id/runs/wait, /runs/stream, and /resumeYesYes. Middleware is an additional execution decision.
AG-UI executionPOST /agui/:routeIdYesYes
Memory candidate managementGET /memory/candidates and approve/reject routes under /memory/candidates/:idNoYes. With src/auth.ts, a caller reviews only its own namespaces unless canReviewMemory admits it.

Apply authorization before dispatch, so no request reaches a B4.run handler unauthenticated. The "B4.run middleware" column is about route execution only. When the app has a thread-access policy, that policy checks per-thread ownership on every thread row above (management, state, cancellation, Agent Protocol execution and AG-UI execution) inside the runtime. It's a second gate behind the outer one. Health and memory-candidate routes are outside it.

Authorize the tenant, not the identifierCopy link to section: Authorize the tenant, not the identifier

Verified claims answer who the caller is. Route parameters, thread_id, AG-UI threadId, tenant strings in route input and memory namespaces answer which resource was requested. Compare the two using records your application keeps.

A sound request path looks like this:

  1. Resolve the verified principal.
  2. Determine its tenant and roles.
  3. Load or check thread ownership.
  4. Forward the request.

A thread identifier proves nothing about ownership, even when it's hard to guess. Use the same rule for state, cancellation, deletion, resume and any custom list endpoint.

Memory needs the same care. memory.resolveScope receives the request principal, so derive user and tenant dimensions from it, and never let model-selected or caller-selected scope strings establish ownership. With an app src/auth.ts, each caller reviews only candidates in its own namespaces (the ones memory.resolveScope gives its principal, plus shared ones), and approving or rejecting anyone else's answers 404 like a missing record. canReviewMemory(principal) on defineAuth admits a reviewer to every namespace. Without an auth file, candidate management spans every namespace, so keep those routes behind outer authentication.

Pass verified identity to toolsCopy link to section: Pass verified identity to tools

For execution routes, outer authentication can forward a signed internal identity header or trusted request context to B4.run. src/auth.ts verifies that trusted value once per request and returns the canonical principal, which middleware and the thread-access policy read as req.principal and tools read as ctx.principal:

src/auth.ts
import { defineAuth, reject } from "@b4run/sdk"
 
export default defineAuth({
  authenticate: async ({ headers }) => {
    const identity = await verifyInternalIdentity(headers.authorization)
    if (!identity) return reject(401, { error: "Unauthorized" })
    return { id: identity.userId, tenantId: identity.tenantId }
  },
})

The model and the request body must never choose those canonical fields. Keep authorization-sensitive tool arguments separate from verified identity, and have the tool derive tenant filters and credentials from ctx.principal.

Inner agent controlsCopy link to section: Inner agent controls

After service authentication and tenant authorization, Access Control composes four narrower controls on each running route:

  • Tool scoping limits which tools the model is offered.
  • Permissions gate commands, paths, approved tools, delegations and selected memory writes.
  • The execution sandbox constrains B4.run's workspace filesystem and shell tools.
  • Guarded delegation decides whether a subagent receives the delegated input.

These controls cover B4.run's own tools, not your application code. Authored tools execute in the app process unless they isolate themselves. Give their database clients, cloud credentials, network access and filesystem behavior least privilege of their own.

Secrets and stored dataCopy link to section: Secrets and stored data

Keep secrets out of b4.config.ts values. The Hono target serializes JSON-compatible configuration into the generated app.mjs build artifact. Use platform bindings or environment variables for secrets, and keep emitted artifacts somewhere their contents stay private.

Protect .env, provider keys, database URLs, internal identity-signing keys and sandbox credentials with the host's secret system. Scrub verified tokens and sensitive tool arguments from logs and traces.

Postgres rows are plaintext application data unless the app or platform adds encryption. Apply transport encryption, storage encryption, backup protection, database roles, row or schema isolation where it fits, and an audited deletion workflow.

Target differencesCopy link to section: Target differences

Each build target draws the service edge in a different place:

  • Node (b4 start, serveRuntime, generated server). Your reverse proxy or Node host provides blanket auth. B4.run middleware covers execution and AG-UI only. Node can use configured sandboxes.
  • Hono. Hono or platform middleware must protect the rooted B4.run app. B4.run execution middleware is included in the static manifest. Filesystem workspace and B4.run sandbox features are gated off this target.
  • LangSmith. src/auth.ts compiles to LangGraph's auth layer, and an ownedThreads policy to its thread handlers; otherwise the platform handles authentication. Generated graph entries don't include B4.run HTTP middleware. Route tool scope and the applicable agent controls remain part of graph materialization.

Check the capabilities of your chosen Deployment Option before assuming a control carries over unchanged between targets.

Production checklistCopy link to section: Production checklist

  • Restrict network access and require authentication on every endpoint group in the table.
  • Map verified principals to application-owned tenant and thread records.
  • Reject cross-tenant reads, state access, runs, resume, cancellation and deletion.
  • Treat memory-candidate routes as privileged administrative operations.
  • Pass canonical identity to tools through trusted middleware context, never model input.
  • Run with least-privilege database roles, provider keys, authored tools and sandbox credentials.
  • Keep secrets out of generated artifacts and redact them from logs and traces.
  • Test middleware-bypassing routes and cross-tenant attempts in the deployed topology.
  • Review all of this again whenever the build target or embedding host changes.