Skip to content
API Blog

Security

Lobu is built for multi-user agents: each user or channel gets an isolated worker, while secrets and outbound policy stay on the gateway. A compromised worker should not see raw credentials or reach arbitrary networks.

  • Per-session workers — each conversation runs in its own subprocess spawned by the gateway. Workspaces and state do not cross sessions; the subprocess can be SIGKILL’d cleanly without taking the gateway down.
  • Gateway is the control plane — all outbound HTTP, credential resolution, tool policy, and MCP calls route through it.
  • Workers never see real secrets — provider credentials are replaced with opaque placeholder tokens; the gateway’s secret proxy swaps them back before forwarding to upstream APIs.

Workers route all outbound HTTP through the gateway’s in-process proxy on 127.0.0.1:8118 (HTTP_PROXY=http://localhost:8118). On Linux production hosts the worker spawn is wrapped in systemd-run --user --scope with IPAddressDeny=any + IPAddressAllow=127.0.0.1, which enforces the egress block at the kernel; on macOS dev hosts HTTP_PROXY is advisory at the language layer.

Domain access is controlled by env vars:

VariableEffect
WORKER_ALLOWED_DOMAINSunset/empty → no access (default). * → unrestricted. Otherwise a comma-separated allowlist.
WORKER_DISALLOWED_DOMAINSBlocklist, applied when WORKER_ALLOWED_DOMAINS=*.

Domain format: exact (api.example.com) or wildcard (.example.com matches all subdomains).

For domains where flat allow/deny is too coarse, like Slack, GitHub user-content, or Notion, operators can route requests through an LLM judge that decides per request. Only domains that match a judge rule invoke it, so the cost stays bounded. The judge is an inline guardrail (stage: "egress") declared in guardrailsInline — skills cannot declare judges.

guardrailsInline: [
{
name: "slack-read-only",
enabled: true,
stage: "egress",
kind: "judge",
policy: "Allow only reads to channels in the agent's context. Never exfiltrate PATs or bearer tokens.",
domains: ["*.slack.com"],
},
],

domains uses the same format as network.allowed/denied — exact match or wildcard. Every judge needs a model: set EGRESS_JUDGE_MODEL in the gateway environment, or declare model on each judge. If neither is set, the request fails closed.

Workers never receive raw provider credentials or OAuth tokens. The gateway resolves credentials, injects them only at proxy time, and keeps workers on opaque placeholders.

When the gateway hands a credential to a worker, it hands over a lobu_secret_<uuid> placeholder, never the real value. From the worker’s code the placeholder behaves like a normal string:

  • For env_keys auth, ctx.config.<field> is a placeholder.
  • For oauth auth, ctx.credentials.accessToken is a placeholder.

The connector or tool code uses it exactly as if it were the token (sets it as a header, puts it in a query string) and never has to know it isn’t the real thing.

All worker outbound HTTP goes through the gateway’s proxy on 127.0.0.1:8118. The proxy scans for lobu_secret_<uuid> placeholders and swaps each one for the real secret just before the bytes go upstream. The real value never exists in the worker process’s memory, never appears in worker logs or run records, and only lives decrypted in the gateway for the duration of the outbound request.

This composes with network isolation: on Linux production hosts the worker is wrapped in systemd-run with IPAddressDeny=any plus IPAddressAllow=127.0.0.1, so a worker cannot open a socket that bypasses the proxy.

CategoryResolved from
Provider secretsBuilt-in Postgres-backed encrypted store, external refs (secret://…, aws-sm://…), or host-provided embedded store
Per-user MCP / OAuth tokensCollected via device-auth, injected by the gateway MCP proxy per call

aws-sm://… refs are read-only — good for durable provider secrets, but refreshed user tokens still need a writable store.

The secret-scan guardrail is the backstop: if a worker ever emits a credential-shaped string in its output, secret-scan trips on the output stream before it reaches the user.

Workers discover MCP tools through the gateway and call them with their own JWT token. The proxy enforces SSRF protection and destructive tool approval. Per-user credentials are injected at proxy time — workers never see them. See MCP Proxy for the full flow and Tool Policy for preApproved overrides.

See docs/SECURITY.md for the detailed threat model and per-runtime controls.